mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 00:35:41 +00:00
e7d23ef0ae
* fixes openziti/ziti#3990 push service and posture state to subscribed SDKs - pushes indexed atomic ServiceChangeSet envelopes to subscribed SDK connections: a full snapshot on subscribe, incremental service changes per RDM scan pass, posture check definition changes as their own entries, and identity-resolved config bodies, all serialized so envelopes hit the wire in index order - pushes per-connection PostureStateChange state (monotonic seq, resync on request) for posture pass/fail, including flips caused by definition edits that mutate no posture data - registers pending RDM identity subscriptions for identities not yet synced to the router and sends an authoritative full sync plus full posture state when the identity arrives; an active push subscription pins the connection's RDM listener - advertises service subscriptions and router data model support on the control-channel capability bitmask; the controller persists each router's capabilities mask and version on the EdgeRouter entity via raft and renders them on the edge APIs, so SDKs can select capable routers before connecting - submits posture per router and corrects MFA posture semantics: pushed expiry is the earliest of timeout and pending wake/unlock grace deadlines, wake/unlock re-pass satisfies the re-prompt, api session tokens whose amr attests TOTP seed the MFA baseline from auth_time only (never iat), and token exchange carries the subject token's auth_time - sends structured denials on dial and bind refusals: posture failures carry the failing check ids, no-policy denials are access denied, unknown services are invalid service, and session token failures are invalid session; the denial's cause no longer rides the wire as an unserializable error - hard-closes accepted SDK connections on edge listener shutdown so clients observe a router going away immediately - adds integration coverage: subscription snapshots and change delivery, poll and push reconciliation as capable routers come and go, posture state and definition-change push, router views over the public SDK API, typed dial errors, MFA baseline seeding, and OIDC token-exchange auth_time preservation - removed RDM capability from SDK, router/controller only
3072 lines
102 KiB
Go
3072 lines
102 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 common
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"os"
|
|
"sort"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/edge-api/rest_model"
|
|
"github.com/openziti/foundation/v2/concurrenz"
|
|
"github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb"
|
|
cmap "github.com/orcaman/concurrent-map/v2"
|
|
"github.com/sirupsen/logrus"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
const (
|
|
IdentityAdded byte = 1
|
|
IdentityUpdated byte = 2
|
|
IdentityDeleted byte = 4
|
|
)
|
|
|
|
// Revocation type values for DataState_Revocation.Type, distinguishing what the
|
|
// revocation's Id identifies so the router can match it against the right token
|
|
// attribute. They mirror rest_model.RevocationTypeEnum — the canonical taxonomy
|
|
// used by the management revocation API and persisted on model.Revocation — so
|
|
// router-side matching and every producer share one vocabulary. An empty type
|
|
// is a legacy revocation with no router-side enforcement.
|
|
const (
|
|
RevocationTypeApiSession = string(rest_model.RevocationTypeEnumAPISESSION) // "API_SESSION"; Id is an api-session id (z_asid)
|
|
RevocationTypeIdentity = string(rest_model.RevocationTypeEnumIDENTITY) // "IDENTITY"; Id is an identity id
|
|
RevocationTypeToken = string(rest_model.RevocationTypeEnumJTI) // "JTI"; Id is a token id (jti)
|
|
)
|
|
|
|
// RouterDataModelConfig contains the configuration values for a RouterDataModel
|
|
type RouterDataModelConfig struct {
|
|
LogSize uint64
|
|
ListenerBufferSize uint
|
|
}
|
|
|
|
// AccessPolicies represents the Identity's access to a Service through many Policies. The PostureChecks provided
|
|
// are referenced by the granting Policies. The PostureChecks for each of the Policies may be evaluated to determine
|
|
// a valid policy and posture access path.
|
|
type AccessPolicies struct {
|
|
Identity *Identity
|
|
Service *Service
|
|
Policies []*ServicePolicy
|
|
PostureChecks map[string]*edge_ctrl_pb.DataState_PostureCheck
|
|
}
|
|
|
|
// serviceAccess tracks the number of dial and bind policies that grant an identity access to a service.
|
|
// It maintains counters for both policy types and tracks the latest index for each to support incremental updates.
|
|
type serviceAccess struct {
|
|
DialPoliciesCount int32
|
|
BindPoliciesCount int32
|
|
|
|
dialPoliciesIndex uint64
|
|
bindPoliciesIndex uint64
|
|
}
|
|
|
|
func (self *serviceAccess) updateServicePolicies(servicePolicy *ServicePolicy, index uint64, add bool) {
|
|
if servicePolicy.IsDial() {
|
|
if add {
|
|
self.DialPoliciesCount++
|
|
} else {
|
|
self.DialPoliciesCount--
|
|
}
|
|
if index > self.dialPoliciesIndex {
|
|
self.dialPoliciesIndex = index
|
|
}
|
|
} else if servicePolicy.IsBind() {
|
|
if add {
|
|
self.BindPoliciesCount++
|
|
} else {
|
|
self.BindPoliciesCount--
|
|
}
|
|
|
|
if index > self.bindPoliciesIndex {
|
|
self.bindPoliciesIndex = index
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *serviceAccess) updatePostureChecksIndex(servicePolicy *ServicePolicy, index uint64) {
|
|
if servicePolicy.IsDial() {
|
|
if index > self.dialPoliciesIndex {
|
|
self.dialPoliciesIndex = index
|
|
}
|
|
} else if servicePolicy.IsBind() {
|
|
if index > self.bindPoliciesIndex {
|
|
self.bindPoliciesIndex = index
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *serviceAccess) IsDialAllowed() bool {
|
|
return self.DialPoliciesCount > 0
|
|
}
|
|
|
|
func (self *serviceAccess) IsBindAllowed() bool {
|
|
return self.BindPoliciesCount > 0
|
|
}
|
|
|
|
func (self *serviceAccess) HasAccess() bool {
|
|
return self.IsDialAllowed() || self.IsBindAllowed()
|
|
}
|
|
|
|
func (self *serviceAccess) ToMap() map[string]interface{} {
|
|
result := map[string]interface{}{}
|
|
|
|
result["dialPoliciesCount"] = self.DialPoliciesCount
|
|
result["bindPoliciesCount"] = self.BindPoliciesCount
|
|
result["dialPoliciesIndex"] = self.dialPoliciesIndex
|
|
result["bindPoliciesIndex"] = self.bindPoliciesIndex
|
|
|
|
return result
|
|
}
|
|
|
|
type DataStateIdentity = edge_ctrl_pb.DataState_Identity
|
|
|
|
// Identity represents an identity in the router data model. An identity is an entity that can access services
|
|
// in the network. It contains hosting configuration for services, application-specific data, service configurations,
|
|
// and tracking information for service policies, service access, and posture checks. The identity tracks which
|
|
// service policies apply to it and maintains denormalized service access information when tracking is enabled.
|
|
type Identity struct {
|
|
Id string
|
|
Name string
|
|
DefaultHostingPrecedence edge_ctrl_pb.TerminatorPrecedence
|
|
DefaultHostingCost uint32
|
|
ServiceHostingPrecedences map[string]edge_ctrl_pb.TerminatorPrecedence
|
|
ServiceHostingCosts map[string]uint32
|
|
AppDataJson []byte
|
|
ServiceConfigs map[string]*edge_ctrl_pb.DataState_ServiceConfigs
|
|
Disabled bool
|
|
|
|
lock sync.Mutex
|
|
ServicePolicies map[string]struct{}
|
|
ServiceAccess map[string]*serviceAccess
|
|
PostureChecks map[string]int32
|
|
identityIndex uint64
|
|
|
|
serviceAccessTrackingEnabled atomic.Bool
|
|
}
|
|
|
|
func (self *Identity) GetId() string {
|
|
return self.Id
|
|
}
|
|
|
|
func (self *Identity) ToProtobuf() *edge_ctrl_pb.DataState_Identity {
|
|
return &edge_ctrl_pb.DataState_Identity{
|
|
Id: self.Id,
|
|
Name: self.Name,
|
|
DefaultHostingPrecedence: self.DefaultHostingPrecedence,
|
|
DefaultHostingCost: self.DefaultHostingCost,
|
|
ServiceHostingPrecedences: self.ServiceHostingPrecedences,
|
|
ServiceHostingCosts: self.ServiceHostingCosts,
|
|
AppDataJson: self.AppDataJson,
|
|
ServiceConfigs: self.ServiceConfigs,
|
|
Disabled: self.Disabled,
|
|
}
|
|
}
|
|
|
|
func (self *Identity) getReferencedConfigs() map[string]struct{} {
|
|
if len(self.ServiceConfigs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
configs := map[string]struct{}{}
|
|
|
|
for _, svcConfigs := range self.ServiceConfigs {
|
|
for _, configId := range svcConfigs.Configs {
|
|
configs[configId] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return configs
|
|
}
|
|
|
|
func (x *Identity) GetServiceConfigsAsMap() map[string]map[string]string {
|
|
if x.ServiceConfigs == nil {
|
|
return nil
|
|
}
|
|
|
|
result := map[string]map[string]string{}
|
|
for serviceId, configs := range x.ServiceConfigs {
|
|
m := map[string]string{}
|
|
for configType, configId := range configs.Configs {
|
|
m[configType] = configId
|
|
}
|
|
result[serviceId] = m
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (self *Identity) IterateServicePolicies(f func(servicePolicyId string)) {
|
|
self.lock.Lock()
|
|
defer self.lock.Unlock()
|
|
for servicePolicyId := range self.ServicePolicies {
|
|
f(servicePolicyId)
|
|
}
|
|
}
|
|
|
|
func (self *Identity) WithLock(f func()) {
|
|
self.lock.Lock()
|
|
defer self.lock.Unlock()
|
|
f()
|
|
}
|
|
|
|
func (self *Identity) NotifyServiceChange(rdm *RouterDataModel) {
|
|
rdm.queueSyncCheck(self.Id)
|
|
}
|
|
|
|
func (self *Identity) updateServiceCount(policy *ServicePolicy, serviceId string, index uint64, add bool) {
|
|
current, present := self.ServiceAccess[serviceId]
|
|
if current == nil {
|
|
current = &serviceAccess{}
|
|
}
|
|
current.updateServicePolicies(policy, index, add)
|
|
if current.HasAccess() {
|
|
if !present {
|
|
self.ServiceAccess[serviceId] = current
|
|
}
|
|
} else if present {
|
|
delete(self.ServiceAccess, serviceId)
|
|
}
|
|
}
|
|
|
|
func (self *Identity) decrementUseCount(m map[string]int32, id string) {
|
|
currentValue, ok := m[id]
|
|
if ok {
|
|
currentValue--
|
|
if currentValue < 1 {
|
|
delete(m, id)
|
|
} else {
|
|
m[id] = currentValue
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *Identity) addedToPolicy(policy *ServicePolicy, index uint64) {
|
|
if _, ok := self.ServicePolicies[policy.Id]; !ok {
|
|
self.ServicePolicies[policy.Id] = struct{}{}
|
|
if self.serviceAccessTrackingEnabled.Load() {
|
|
policy.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
self.updateServiceCount(policy, serviceId, index, true)
|
|
})
|
|
}
|
|
|
|
if !policy.Services.IsEmpty() {
|
|
policy.PostureChecks.IterCb(func(key string, _ struct{}) {
|
|
self.PostureChecks[key]++
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *Identity) removedFromPolicy(policy *ServicePolicy, index uint64) {
|
|
if _, ok := self.ServicePolicies[policy.Id]; ok {
|
|
delete(self.ServicePolicies, policy.Id)
|
|
|
|
if self.serviceAccessTrackingEnabled.Load() {
|
|
policy.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
self.updateServiceCount(policy, serviceId, index, false)
|
|
})
|
|
}
|
|
|
|
if !policy.Services.IsEmpty() {
|
|
policy.PostureChecks.IterCb(func(postureCheckId string, _ struct{}) {
|
|
self.decrementUseCount(self.PostureChecks, postureCheckId)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *Identity) servicesAddedToPolicy(policy *ServicePolicy, serviceIds []string, index uint64) {
|
|
if self.serviceAccessTrackingEnabled.Load() {
|
|
if _, ok := self.ServicePolicies[policy.Id]; ok {
|
|
for _, serviceId := range serviceIds {
|
|
self.updateServiceCount(policy, serviceId, index, true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *Identity) servicesRemovedFromPolicy(policy *ServicePolicy, serviceIds []string, index uint64) {
|
|
if self.serviceAccessTrackingEnabled.Load() {
|
|
if _, ok := self.ServicePolicies[policy.Id]; ok {
|
|
for _, serviceId := range serviceIds {
|
|
self.updateServiceCount(policy, serviceId, index, false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *Identity) postureChecksAddedToPolicy(policy *ServicePolicy, postureCheckIds []string, index uint64) {
|
|
if _, ok := self.ServicePolicies[policy.Id]; ok {
|
|
for _, postureCheckId := range postureCheckIds {
|
|
self.PostureChecks[postureCheckId]++
|
|
}
|
|
}
|
|
|
|
if self.serviceAccessTrackingEnabled.Load() {
|
|
policy.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
access := self.ServiceAccess[serviceId]
|
|
access.updatePostureChecksIndex(policy, index)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (self *Identity) postureChecksRemovedFromPolicy(policy *ServicePolicy, postureCheckIds []string, index uint64) {
|
|
if _, ok := self.ServicePolicies[policy.Id]; ok {
|
|
for _, postureCheckId := range postureCheckIds {
|
|
self.decrementUseCount(self.PostureChecks, postureCheckId)
|
|
}
|
|
}
|
|
|
|
if self.serviceAccessTrackingEnabled.Load() {
|
|
policy.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
access := self.ServiceAccess[serviceId]
|
|
access.updatePostureChecksIndex(policy, index)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (self *Identity) Equals(other *Identity) bool {
|
|
log := pfxlog.Logger().WithField("identity", self.identityIndex)
|
|
if self.Disabled != other.Disabled {
|
|
log.Debug("identity updated, disabled flag changed")
|
|
return false
|
|
}
|
|
|
|
if self.Name != other.Name {
|
|
log.Debug("identity updated, name changed")
|
|
return false
|
|
}
|
|
|
|
if string(self.AppDataJson) != string(other.AppDataJson) {
|
|
log.Debug("identity updated, appDataJson changed")
|
|
return false
|
|
}
|
|
|
|
if self.DefaultHostingPrecedence != other.DefaultHostingPrecedence {
|
|
log.Debug("identity updated, default hosting precedence changed")
|
|
return false
|
|
}
|
|
|
|
if self.DefaultHostingCost != other.DefaultHostingCost {
|
|
log.Debug("identity updated, default hosting host changed")
|
|
return false
|
|
}
|
|
|
|
if len(self.ServiceHostingPrecedences) != len(other.ServiceHostingPrecedences) {
|
|
log.Debug("identity updated, number of service hosting precedences changed")
|
|
return false
|
|
}
|
|
|
|
if len(self.ServiceHostingCosts) != len(other.ServiceHostingCosts) {
|
|
log.Debug("identity updated, number of service hosting costs changed")
|
|
return false
|
|
}
|
|
|
|
if len(self.ServiceConfigs) != len(other.ServiceConfigs) {
|
|
log.Debug("identity updated, number of service configs changed")
|
|
return false
|
|
}
|
|
|
|
for k, v := range self.ServiceHostingPrecedences {
|
|
v2, ok := other.ServiceHostingPrecedences[k]
|
|
if !ok || v != v2 {
|
|
log.Debug("identity updated, a service hosting precedence changed")
|
|
return false
|
|
}
|
|
}
|
|
|
|
for k, v := range self.ServiceHostingCosts {
|
|
v2, ok := other.ServiceHostingCosts[k]
|
|
if !ok || v != v2 {
|
|
log.Debug("identity updated, a service hosting cost changed")
|
|
return false
|
|
}
|
|
}
|
|
|
|
for k, v := range self.ServiceConfigs {
|
|
v2, ok := other.ServiceConfigs[k]
|
|
if !ok || !maps.Equal(v.Configs, v2.Configs) {
|
|
log.Debug("identity updated, a service config changed")
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type DataStateConfigType = edge_ctrl_pb.DataState_ConfigType
|
|
|
|
// ConfigType represents a configuration type that defines the schema or category for configuration data.
|
|
// Config instances reference a ConfigType to indicate what kind of configuration they contain.
|
|
type ConfigType struct {
|
|
Id string
|
|
Name string
|
|
Target string
|
|
index uint64
|
|
}
|
|
|
|
func (self *ConfigType) GetId() string {
|
|
return self.Id
|
|
}
|
|
|
|
func (self *ConfigType) ToProtobuf() *edge_ctrl_pb.DataState_ConfigType {
|
|
return &edge_ctrl_pb.DataState_ConfigType{
|
|
Id: self.Id,
|
|
Name: self.Name,
|
|
Target: self.Target,
|
|
}
|
|
}
|
|
|
|
func (self *ConfigType) Equals(other *ConfigType) bool {
|
|
return self.Name == other.Name && self.Target == other.Target
|
|
}
|
|
|
|
type DataStateConfig = edge_ctrl_pb.DataState_Config
|
|
|
|
// Config represents a configuration instance that contains JSON data of a specific ConfigType.
|
|
// Configs can be associated with services and identities to provide application-specific settings.
|
|
// The struct tracks which services and identities reference this configuration for efficient updates.
|
|
type Config struct {
|
|
Id string
|
|
TypeId string
|
|
Name string
|
|
DataJson string
|
|
services cmap.ConcurrentMap[string, struct{}]
|
|
identities cmap.ConcurrentMap[string, struct{}]
|
|
index uint64
|
|
}
|
|
|
|
func (self *Config) GetId() string {
|
|
return self.Id
|
|
}
|
|
|
|
func (self *Config) ToProtobuf() *edge_ctrl_pb.DataState_Config {
|
|
return &edge_ctrl_pb.DataState_Config{
|
|
Id: self.Id,
|
|
TypeId: self.TypeId,
|
|
Name: self.Name,
|
|
DataJson: self.DataJson,
|
|
}
|
|
}
|
|
|
|
func (self *Config) Equals(other *Config) bool {
|
|
if other == nil {
|
|
return self == nil
|
|
}
|
|
|
|
if self.Id != other.Id {
|
|
return false
|
|
}
|
|
|
|
if self.Name != other.Name {
|
|
return false
|
|
}
|
|
|
|
if self.TypeId != other.TypeId {
|
|
return false
|
|
}
|
|
|
|
if self.DataJson != other.DataJson {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type DataStateService = edge_ctrl_pb.DataState_Service
|
|
|
|
// Service represents a network service that identities can dial or bind to. A service defines
|
|
// the network resource being accessed and can require encryption, have associated configurations,
|
|
// and be governed by service policies that control which identities have access.
|
|
type Service struct {
|
|
Id string
|
|
Name string
|
|
EncryptionRequired bool
|
|
Configs []string
|
|
servicePolicies cmap.ConcurrentMap[string, struct{}]
|
|
index uint64
|
|
}
|
|
|
|
func (self *Service) GetId() string {
|
|
return self.Id
|
|
}
|
|
|
|
func (self *Service) ToProtobuf() *edge_ctrl_pb.DataState_Service {
|
|
return &edge_ctrl_pb.DataState_Service{
|
|
Id: self.Id,
|
|
Name: self.Name,
|
|
EncryptionRequired: self.EncryptionRequired,
|
|
Configs: self.Configs,
|
|
}
|
|
}
|
|
|
|
func (self *Service) GetIndex() uint64 {
|
|
return self.index
|
|
}
|
|
|
|
type DataStatePostureCheck = edge_ctrl_pb.DataState_PostureCheck
|
|
|
|
// PostureCheck represents a security posture requirement that must be satisfied before an identity
|
|
// can access a service. Posture checks can verify things like OS version, running processes, domain
|
|
// membership, MFA status, and other security attributes. They are associated with service policies
|
|
// to enforce security requirements.
|
|
type PostureCheck struct {
|
|
*DataStatePostureCheck
|
|
servicePolicies cmap.ConcurrentMap[string, struct{}]
|
|
index uint64
|
|
}
|
|
|
|
type DataStateServicePolicy = edge_ctrl_pb.DataState_ServicePolicy
|
|
|
|
// ServicePolicy defines the authorization rules that control which identities can access which services.
|
|
// A policy can be either a dial policy (allowing identities to connect to services) or a bind policy
|
|
// (allowing identities to host services). Policies can include posture checks that must be satisfied
|
|
// for access to be granted. The struct tracks the sets of identities, services, and posture checks
|
|
// associated with the policy, along with indices for tracking incremental updates.
|
|
type ServicePolicy struct {
|
|
Id string
|
|
Name string
|
|
PolicyType edge_ctrl_pb.PolicyType
|
|
Services cmap.ConcurrentMap[string, struct{}]
|
|
PostureChecks cmap.ConcurrentMap[string, struct{}]
|
|
Identities cmap.ConcurrentMap[string, struct{}]
|
|
servicesIndex uint64
|
|
postureChecksIndex uint64
|
|
}
|
|
|
|
func (self *ServicePolicy) GetId() string {
|
|
return self.Id
|
|
}
|
|
|
|
func (self *ServicePolicy) ToProtobuf() *edge_ctrl_pb.DataState_ServicePolicy {
|
|
return &edge_ctrl_pb.DataState_ServicePolicy{
|
|
Id: self.Id,
|
|
Name: self.Name,
|
|
PolicyType: self.PolicyType,
|
|
}
|
|
}
|
|
|
|
func (self *ServicePolicy) IsDial() bool {
|
|
return self.PolicyType == edge_ctrl_pb.PolicyType_DialPolicy
|
|
}
|
|
|
|
func (self *ServicePolicy) IsBind() bool {
|
|
return self.PolicyType == edge_ctrl_pb.PolicyType_BindPolicy
|
|
}
|
|
|
|
// RouterDataModel represents a sub-set of a controller's data model. Enough to validate an identities access to dial/bind
|
|
// a service through policies and posture checks. RouterDataModel can operate in two modes: sender (controller) and
|
|
// receiver (router). Sender mode allows a controller support an event cache that supports replays for routers connecting
|
|
// for the first time/after disconnects. Receive mode does not maintain an event cache and does not support replays.
|
|
// It instead is used as a reference data structure for authorization computations.
|
|
type RouterDataModel struct {
|
|
ConfigTypes cmap.ConcurrentMap[string, *ConfigType] `json:"configTypes"`
|
|
Configs cmap.ConcurrentMap[string, *Config] `json:"configs"`
|
|
Identities cmap.ConcurrentMap[string, *Identity] `json:"identities"`
|
|
Services cmap.ConcurrentMap[string, *Service] `json:"services"`
|
|
Routers cmap.ConcurrentMap[string, *edge_ctrl_pb.DataState_Router] `json:"routers"`
|
|
ServicePolicies cmap.ConcurrentMap[string, *ServicePolicy] `json:"servicePolicies"`
|
|
PostureChecks cmap.ConcurrentMap[string, *PostureCheck] `json:"postureChecks"`
|
|
PublicKeys cmap.ConcurrentMap[string, *edge_ctrl_pb.DataState_PublicKey] `json:"publicKeys"`
|
|
Revocations cmap.ConcurrentMap[string, *edge_ctrl_pb.DataState_Revocation] `json:"revocations"`
|
|
cachedPublicKeys concurrenz.AtomicValue[map[string]crypto.PublicKey]
|
|
|
|
// serviceNameIndex maps service name -> service id, kept in sync with Services.
|
|
// Controller enforces unique service names, so this is a 1:1 mapping.
|
|
serviceNameIndex cmap.ConcurrentMap[string, string]
|
|
|
|
terminatorIdCache cmap.ConcurrentMap[string, string]
|
|
|
|
lastSaveIndex *uint64
|
|
|
|
subscriptions cmap.ConcurrentMap[string, *IdentitySubscription]
|
|
updatedIdentities cmap.ConcurrentMap[string, byte]
|
|
subscriptionUpdateNotify chan struct{}
|
|
|
|
events chan subscriberEvent
|
|
scanSubsNotify chan struct{}
|
|
|
|
closeNotify <-chan struct{}
|
|
stopNotify chan struct{}
|
|
stopped atomic.Bool
|
|
|
|
// timelineId identifies the database that events are flowing from. This will be reset whenever we change the
|
|
// underlying datastore
|
|
timelineId string
|
|
|
|
// selfRouterId, when non-empty, is the ID of the local router. Used by
|
|
// HandleRouterEvent on the receiver side to GC router-target Config orphans
|
|
// when the local Router entity's Configs list shrinks. Set at construction
|
|
// time. Empty for non-receiver views (controller-side, validate parsing).
|
|
selfRouterId string
|
|
|
|
// routerConfigSubscriber receives notifications for Config events whose
|
|
// ConfigType.Target is "router". Set via SetRouterConfigSubscriber after
|
|
// construction; a nil-pointer Load means no dispatch.
|
|
routerConfigSubscriber atomic.Pointer[RouterConfigEventSubscriber]
|
|
|
|
lock sync.Mutex
|
|
index uint64
|
|
}
|
|
|
|
// SelfRouterId returns the locally-set router ID, or "" if not set (e.g. on the
|
|
// controller-side RDM).
|
|
func (rdm *RouterDataModel) SelfRouterId() string {
|
|
return rdm.selfRouterId
|
|
}
|
|
|
|
// NewBareRouterDataModel creates a new RouterDataModel with no buffers, listeners or subscriptions.
|
|
// Pass the local router ID to enable receiver-side GC of router-target Config orphans on
|
|
// self-Router updates; pass "" for non-receiver views (controller-side construction or
|
|
// validate-snapshot parsing where the local "self" is irrelevant).
|
|
func NewBareRouterDataModel(routerId string) *RouterDataModel {
|
|
return &RouterDataModel{
|
|
ConfigTypes: cmap.New[*ConfigType](),
|
|
Configs: cmap.New[*Config](),
|
|
Identities: cmap.New[*Identity](),
|
|
Services: cmap.New[*Service](),
|
|
Routers: cmap.New[*edge_ctrl_pb.DataState_Router](),
|
|
ServicePolicies: cmap.New[*ServicePolicy](),
|
|
PostureChecks: cmap.New[*PostureCheck](),
|
|
PublicKeys: cmap.New[*edge_ctrl_pb.DataState_PublicKey](),
|
|
Revocations: cmap.New[*edge_ctrl_pb.DataState_Revocation](),
|
|
terminatorIdCache: cmap.New[string](),
|
|
subscriptions: cmap.New[*IdentitySubscription](),
|
|
selfRouterId: routerId,
|
|
serviceNameIndex: cmap.New[string](),
|
|
}
|
|
}
|
|
|
|
// NewReceiverRouterDataModel creates a new RouterDataModel that does not store events. listenerBufferSize affects the
|
|
// buffer size of channels returned to listeners of the data model. routerId is the local router's ID and enables
|
|
// receiver-side GC of router-target Config orphans on self-Router updates.
|
|
func NewReceiverRouterDataModel(routerId string, closeNotify <-chan struct{}) *RouterDataModel {
|
|
result := &RouterDataModel{
|
|
ConfigTypes: cmap.New[*ConfigType](),
|
|
Configs: cmap.New[*Config](),
|
|
Identities: cmap.New[*Identity](),
|
|
Services: cmap.New[*Service](),
|
|
Routers: cmap.New[*edge_ctrl_pb.DataState_Router](),
|
|
ServicePolicies: cmap.New[*ServicePolicy](),
|
|
PostureChecks: cmap.New[*PostureCheck](),
|
|
PublicKeys: cmap.New[*edge_ctrl_pb.DataState_PublicKey](),
|
|
Revocations: cmap.New[*edge_ctrl_pb.DataState_Revocation](),
|
|
subscriptions: cmap.New[*IdentitySubscription](),
|
|
updatedIdentities: cmap.New[byte](),
|
|
subscriptionUpdateNotify: make(chan struct{}, 1),
|
|
events: make(chan subscriberEvent, 250),
|
|
scanSubsNotify: make(chan struct{}, 1),
|
|
closeNotify: closeNotify,
|
|
stopNotify: make(chan struct{}),
|
|
terminatorIdCache: cmap.New[string](),
|
|
selfRouterId: routerId,
|
|
serviceNameIndex: cmap.New[string](),
|
|
}
|
|
go result.processSubscriberEvents()
|
|
return result
|
|
}
|
|
|
|
// NewReceiverRouterDataModelFromDataState creates a new RouterDataModel that does not store events. listenerBufferSize affects the
|
|
// buffer size of channels returned to listeners of the data model. routerId is the local router's ID.
|
|
func NewReceiverRouterDataModelFromDataState(routerId string, dataState *edge_ctrl_pb.DataState, closeNotify <-chan struct{}) *RouterDataModel {
|
|
result := &RouterDataModel{
|
|
ConfigTypes: cmap.New[*ConfigType](),
|
|
Configs: cmap.New[*Config](),
|
|
Identities: cmap.New[*Identity](),
|
|
Services: cmap.New[*Service](),
|
|
Routers: cmap.New[*edge_ctrl_pb.DataState_Router](),
|
|
ServicePolicies: cmap.New[*ServicePolicy](),
|
|
PostureChecks: cmap.New[*PostureCheck](),
|
|
PublicKeys: cmap.New[*edge_ctrl_pb.DataState_PublicKey](),
|
|
Revocations: cmap.New[*edge_ctrl_pb.DataState_Revocation](),
|
|
subscriptions: cmap.New[*IdentitySubscription](),
|
|
updatedIdentities: cmap.New[byte](),
|
|
subscriptionUpdateNotify: make(chan struct{}, 1),
|
|
events: make(chan subscriberEvent, 250),
|
|
scanSubsNotify: make(chan struct{}, 1),
|
|
closeNotify: closeNotify,
|
|
stopNotify: make(chan struct{}),
|
|
timelineId: dataState.TimelineId,
|
|
terminatorIdCache: cmap.New[string](),
|
|
selfRouterId: routerId,
|
|
serviceNameIndex: cmap.New[string](),
|
|
}
|
|
|
|
if tIdCache, ok := dataState.Caches[edge_ctrl_pb.CacheType_TerminatorIds.String()]; ok && tIdCache != nil && tIdCache.Data != nil {
|
|
for k, v := range tIdCache.Data {
|
|
result.terminatorIdCache.Set(k, string(v))
|
|
}
|
|
}
|
|
|
|
go result.processSubscriberEvents()
|
|
|
|
result.WhileLocked(func(u uint64) {
|
|
for _, event := range dataState.Events {
|
|
result.Handle(dataState.EndIndex, event)
|
|
}
|
|
result.SetCurrentIndex(dataState.EndIndex)
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
// NewReceiverRouterDataModelFromExisting creates a new RouterDataModel that does not store events. listenerBufferSize affects the
|
|
// buffer size of channels returned to listeners of the data model. routerId is the local router's ID.
|
|
func NewReceiverRouterDataModelFromExisting(routerId string, existing *RouterDataModel, closeNotify <-chan struct{}) *RouterDataModel {
|
|
result := &RouterDataModel{
|
|
ConfigTypes: existing.ConfigTypes,
|
|
Configs: existing.Configs,
|
|
Identities: existing.Identities,
|
|
Services: existing.Services,
|
|
Routers: existing.Routers,
|
|
ServicePolicies: existing.ServicePolicies,
|
|
PostureChecks: existing.PostureChecks,
|
|
PublicKeys: existing.PublicKeys,
|
|
cachedPublicKeys: existing.cachedPublicKeys,
|
|
Revocations: existing.Revocations,
|
|
subscriptions: cmap.New[*IdentitySubscription](),
|
|
updatedIdentities: cmap.New[byte](),
|
|
subscriptionUpdateNotify: make(chan struct{}, 1),
|
|
events: make(chan subscriberEvent, 250),
|
|
scanSubsNotify: make(chan struct{}, 1),
|
|
closeNotify: closeNotify,
|
|
stopNotify: make(chan struct{}),
|
|
timelineId: existing.timelineId,
|
|
terminatorIdCache: existing.terminatorIdCache,
|
|
selfRouterId: routerId,
|
|
serviceNameIndex: existing.serviceNameIndex,
|
|
}
|
|
currentIndex := existing.CurrentIndex()
|
|
result.SetCurrentIndex(currentIndex)
|
|
go result.processSubscriberEvents()
|
|
return result
|
|
}
|
|
|
|
// NewReceiverRouterDataModelFromFile creates a new RouterDataModel that does not store events and is initialized from
|
|
// a file backup. listenerBufferSize affects the buffer size of channels returned to listeners of the data model.
|
|
// routerId is the local router's ID.
|
|
func NewReceiverRouterDataModelFromFile(routerId string, path string, closeNotify <-chan struct{}) (*RouterDataModel, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
gz, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = gz.Close() }()
|
|
|
|
data, err := io.ReadAll(gz)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
state := &edge_ctrl_pb.DataState{}
|
|
if err = proto.Unmarshal(data, state); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rdm := NewReceiverRouterDataModelFromDataState(routerId, state, closeNotify)
|
|
rdm.lastSaveIndex = &state.EndIndex
|
|
|
|
return rdm, nil
|
|
}
|
|
|
|
func (rdm *RouterDataModel) WhileLocked(callback func(uint64)) {
|
|
rdm.lock.Lock()
|
|
defer rdm.lock.Unlock()
|
|
|
|
callback(rdm.CurrentIndex())
|
|
}
|
|
|
|
func (rdm *RouterDataModel) SetCurrentIndex(index uint64) {
|
|
atomic.StoreUint64(&rdm.index, index)
|
|
}
|
|
|
|
func (rdm *RouterDataModel) CurrentIndex() uint64 {
|
|
return atomic.LoadUint64(&rdm.index)
|
|
}
|
|
|
|
func (rdm *RouterDataModel) processSubscriberEvents() {
|
|
for !rdm.stopped.Load() {
|
|
select {
|
|
case <-rdm.closeNotify:
|
|
return
|
|
case <-rdm.stopNotify:
|
|
return
|
|
case evt := <-rdm.events:
|
|
evt.process(rdm)
|
|
case <-rdm.scanSubsNotify:
|
|
start := time.Now()
|
|
count := rdm.updatedIdentities.Count()
|
|
for entry := range rdm.updatedIdentities.IterBuffered() {
|
|
rdm.updatedIdentities.Remove(entry.Key) // do all checks, so we can just remove the flag entirely
|
|
|
|
if entry.Val&IdentityDeleted == IdentityDeleted {
|
|
rdm.checkSubsForDeletedIdentity(entry.Key, false)
|
|
continue
|
|
}
|
|
|
|
if entry.Val&IdentityAdded == IdentityAdded {
|
|
rdm.checkSubsForNewIdentity(entry.Key, false)
|
|
}
|
|
|
|
if entry.Val&IdentityUpdated == IdentityUpdated {
|
|
rdm.syncSubscriptionIfRequired(entry.Key, false)
|
|
}
|
|
}
|
|
pfxlog.Logger().
|
|
WithField("processed", count).
|
|
WithField("subs", rdm.subscriptions.Count()).
|
|
Debugf("sync all pending subscribers in %v", time.Since(start))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) Stop() {
|
|
if rdm.stopped.CompareAndSwap(false, true) {
|
|
close(rdm.stopNotify)
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) GetTimelineId() string {
|
|
return rdm.timelineId
|
|
}
|
|
|
|
// ApplyChangeSet applies the given event to the router data model.
|
|
func (rdm *RouterDataModel) ApplyChangeSet(change *edge_ctrl_pb.DataState_ChangeSet) error {
|
|
logger := pfxlog.Logger().
|
|
WithField("index", change.Index).
|
|
WithField("synthetic", change.IsSynthetic).
|
|
WithField("entries", len(change.Changes))
|
|
|
|
err := rdm.Store(change, func(index uint64, change *edge_ctrl_pb.DataState_ChangeSet) {
|
|
for idx, event := range change.Changes {
|
|
logger.
|
|
WithField("entry", idx).
|
|
WithField("action", event.Action.String()).
|
|
WithField("type", fmt.Sprintf("%T", event.Model)).
|
|
WithField("summary", event.Summarize()).
|
|
Debug("handling change set entry")
|
|
rdm.Handle(index, event)
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
if len(change.Changes) > 0 {
|
|
logger = logger.
|
|
WithField("action", change.Changes[0].Action.String()).
|
|
WithField("type", fmt.Sprintf("%T", change.Changes[0].Model))
|
|
}
|
|
|
|
logger.WithError(err).Error("could not apply change set")
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type GapDetectedError struct {
|
|
CurrentIndex uint64
|
|
PreviousIndex uint64
|
|
ReceivedIndex uint64
|
|
}
|
|
|
|
func (e *GapDetectedError) Error() string {
|
|
return fmt.Sprintf("gap detected: router index %d, event previousIndex %d, event index %d",
|
|
e.CurrentIndex, e.PreviousIndex, e.ReceivedIndex)
|
|
}
|
|
|
|
func (rdm *RouterDataModel) Store(event *edge_ctrl_pb.DataState_ChangeSet, onSuccess OnStoreSuccess) error {
|
|
rdm.lock.Lock()
|
|
defer rdm.lock.Unlock()
|
|
|
|
// Synthetic events are not backed by any kind of data store that provides and index. They are not stored and
|
|
// trigger the on success callback immediately.
|
|
if event.IsSynthetic {
|
|
onSuccess(event.Index, event)
|
|
return nil
|
|
}
|
|
|
|
// All accesses to rdm.index go through atomic ops so that lock-free readers
|
|
// (CurrentIndex) observe the value written here. The lock still serializes
|
|
// the read-modify-write so two writers can't both pass the gap check.
|
|
currentIndex := atomic.LoadUint64(&rdm.index)
|
|
|
|
if currentIndex > 0 && currentIndex >= event.Index {
|
|
return fmt.Errorf("out of order event detected, currentIndex: %d, receivedIndex: %d, type :%T", currentIndex, event.Index, rdm)
|
|
}
|
|
|
|
// Gap detection via previousIndex chain.
|
|
// PreviousIndex == 0 means: backwards-compatible controller (didn't set it), or first event ever.
|
|
if event.PreviousIndex > 0 && currentIndex > 0 && event.PreviousIndex != currentIndex {
|
|
return &GapDetectedError{
|
|
CurrentIndex: currentIndex,
|
|
PreviousIndex: event.PreviousIndex,
|
|
ReceivedIndex: event.Index,
|
|
}
|
|
}
|
|
|
|
atomic.StoreUint64(&rdm.index, event.Index)
|
|
|
|
if onSuccess != nil {
|
|
onSuccess(event.Index, event)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rdm *RouterDataModel) Handle(index uint64, event *edge_ctrl_pb.DataState_Event) {
|
|
switch typedModel := event.Model.(type) {
|
|
case *edge_ctrl_pb.DataState_Event_ConfigType:
|
|
rdm.HandleConfigTypeEvent(index, event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_Config:
|
|
rdm.HandleConfigEvent(index, event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_Identity:
|
|
rdm.HandleIdentityEvent(index, event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_Service:
|
|
rdm.HandleServiceEvent(index, event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_ServicePolicy:
|
|
rdm.HandleServicePolicyEvent(index, event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_PostureCheck:
|
|
rdm.HandlePostureCheckEvent(index, event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_PublicKey:
|
|
rdm.HandlePublicKeyEvent(event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_Revocation:
|
|
rdm.HandleRevocationEvent(event, typedModel)
|
|
case *edge_ctrl_pb.DataState_Event_ServicePolicyChange:
|
|
rdm.HandleServicePolicyChange(index, typedModel.ServicePolicyChange)
|
|
case *edge_ctrl_pb.DataState_Event_Router:
|
|
rdm.HandleRouterEvent(event, typedModel)
|
|
}
|
|
}
|
|
|
|
// HandleRouterEvent applies a Router create/update/delete delta to the RDM cache.
|
|
// On the receiver side, when the event is for the local router (selfRouterId)
|
|
// and the action is not Delete, also GC any router-target configs in
|
|
// rdm.Configs whose IDs are no longer referenced by the new Router.Configs
|
|
// list. This keeps the receiver's Configs map aligned with the assignment
|
|
// without requiring the controller to send synthetic remove events.
|
|
//
|
|
// Why diff against the previously-cached Router rather than scanning Configs:
|
|
// the sender (see RouterDataModelSender.FilterEventsForRouter) emits a
|
|
// synthetic Config event for every entry in the new Router.Configs list
|
|
// immediately before the Router event itself. So when this handler runs the
|
|
// configs the new list references are already in rdm.Configs — the only
|
|
// thing to GC is whatever was in the *previous* Configs list but isn't in
|
|
// the new one. That set is the diff against the previously-cached Router,
|
|
// no full Configs walk needed. Every entry in prev.Configs is by definition
|
|
// a router-target config (the controller enforces target=router on Router.
|
|
// Configs), so we don't have to re-check the ConfigType either.
|
|
func (rdm *RouterDataModel) HandleRouterEvent(event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_Router) {
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
rdm.Routers.Remove(model.Router.Id)
|
|
return
|
|
}
|
|
|
|
prev, _ := rdm.Routers.Get(model.Router.Id)
|
|
rdm.Routers.Set(model.Router.Id, model.Router)
|
|
|
|
selfId := rdm.SelfRouterId()
|
|
if selfId == "" || model.Router.Id != selfId || prev == nil {
|
|
return
|
|
}
|
|
|
|
keep := make(map[string]struct{}, len(model.Router.Configs))
|
|
for _, id := range model.Router.Configs {
|
|
keep[id] = struct{}{}
|
|
}
|
|
|
|
for _, oldId := range prev.Configs {
|
|
if _, retained := keep[oldId]; retained {
|
|
continue
|
|
}
|
|
cfg, ok := rdm.Configs.Get(oldId)
|
|
if !ok {
|
|
continue
|
|
}
|
|
rdm.Configs.Remove(oldId)
|
|
// Dispatch the remove so the managed-config subscriber tears
|
|
// down the listener / dialer for this config. Without this,
|
|
// removing a Config from a router's Configs list would silently
|
|
// leave the router-side listener bound.
|
|
rdm.dispatchRouterConfigRemove(cfg.TypeId)
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) queueEvent(event subscriberEvent) {
|
|
if rdm.events != nil {
|
|
rdm.events <- event
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) SyncAllSubscribers() {
|
|
evt := syncAllSubscribersEvent{completeNotify: make(chan struct{})}
|
|
rdm.queueEvent(evt)
|
|
select {
|
|
case <-evt.completeNotify:
|
|
case <-rdm.closeNotify:
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) markIdentityForCheck(identityId string, checkType byte) {
|
|
rdm.updatedIdentities.Upsert(identityId, 0, func(exist bool, valueInMap byte, newValue byte) byte {
|
|
return valueInMap | checkType
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) markIdentityCheckComplete(identityId string, checkType byte) bool {
|
|
flagWasSet := false
|
|
rdm.updatedIdentities.Upsert(identityId, 0, func(exist bool, valueInMap byte, newValue byte) byte {
|
|
flagWasSet = valueInMap&checkType != 0
|
|
return valueInMap &^ checkType
|
|
})
|
|
|
|
rdm.updatedIdentities.RemoveCb(identityId, func(key string, v byte, exists bool) bool {
|
|
return exists && v == 0
|
|
})
|
|
|
|
return flagWasSet
|
|
}
|
|
|
|
// HandleIdentityEvent will apply the delta event to the router data model. It is not restricted by index calculations.
|
|
// Use ApplyIdentityEvent for event logged event handling. This method is generally meant for bulk loading of data
|
|
// during startup.
|
|
func (rdm *RouterDataModel) HandleIdentityEvent(index uint64, event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_Identity) {
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
var cleanupActions []*edge_ctrl_pb.DataState_Event
|
|
|
|
rdm.withLockedIdentity(model.Identity.Id, func(identity *Identity) {
|
|
rdm.removeConfigReferences(identity)
|
|
|
|
for servicePolicyId := range identity.ServicePolicies {
|
|
// If a service is deleted, the service policy changes should come in first, so this should always be a no-op
|
|
if servicePolicy, _ := rdm.ServicePolicies.Get(servicePolicyId); servicePolicy != nil {
|
|
if servicePolicy.Identities.Has(model.Identity.Id) {
|
|
cleanupActions = append(cleanupActions, &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
IsSynthetic: false,
|
|
Model: &edge_ctrl_pb.DataState_Event_ServicePolicyChange{
|
|
ServicePolicyChange: &edge_ctrl_pb.DataState_ServicePolicyChange{
|
|
PolicyId: servicePolicyId,
|
|
RelatedEntityIds: []string{model.Identity.Id},
|
|
RelatedEntityType: edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedIdentity,
|
|
Add: false,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
for _, cleanupAction := range cleanupActions {
|
|
rdm.Handle(index, cleanupAction)
|
|
}
|
|
|
|
rdm.Identities.Remove(model.Identity.Id)
|
|
rdm.queueIdentityDeletedSubCheck(model.Identity.Id)
|
|
|
|
} else {
|
|
identity := &Identity{
|
|
Id: model.Identity.Id,
|
|
Name: model.Identity.Name,
|
|
DefaultHostingPrecedence: model.Identity.DefaultHostingPrecedence,
|
|
DefaultHostingCost: model.Identity.DefaultHostingCost,
|
|
ServiceHostingPrecedences: model.Identity.ServiceHostingPrecedences,
|
|
ServiceHostingCosts: model.Identity.ServiceHostingCosts,
|
|
AppDataJson: model.Identity.AppDataJson,
|
|
ServiceConfigs: model.Identity.ServiceConfigs,
|
|
Disabled: model.Identity.Disabled,
|
|
identityIndex: index,
|
|
}
|
|
|
|
rdm.Identities.Upsert(model.Identity.Id, nil, func(exist bool, valueInMap *Identity, newValue *Identity) *Identity {
|
|
if valueInMap == nil {
|
|
identity.ServicePolicies = map[string]struct{}{}
|
|
identity.ServiceAccess = map[string]*serviceAccess{}
|
|
identity.PostureChecks = map[string]int32{}
|
|
} else {
|
|
rdm.removeConfigReferences(valueInMap)
|
|
|
|
identity.ServicePolicies = valueInMap.ServicePolicies
|
|
identity.ServiceAccess = valueInMap.ServiceAccess
|
|
identity.PostureChecks = valueInMap.PostureChecks
|
|
identity.serviceAccessTrackingEnabled.Store(valueInMap.serviceAccessTrackingEnabled.Load())
|
|
}
|
|
|
|
rdm.addConfigReferences(identity)
|
|
|
|
return identity
|
|
})
|
|
|
|
if event.Action == edge_ctrl_pb.DataState_Create {
|
|
rdm.queueNewIdentitySubCheck(identity.Id)
|
|
} else if event.Action == edge_ctrl_pb.DataState_Update {
|
|
rdm.queueSyncCheck(identity.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) removeConfigReferences(identity *Identity) {
|
|
configs := identity.getReferencedConfigs()
|
|
|
|
for configId := range configs {
|
|
if config, _ := rdm.Configs.Get(configId); config != nil {
|
|
config.identities.Remove(identity.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) addConfigReferences(identity *Identity) {
|
|
configs := identity.getReferencedConfigs()
|
|
|
|
for configId := range configs {
|
|
if config, _ := rdm.Configs.Get(configId); config != nil {
|
|
config.identities.Set(identity.Id, struct{}{})
|
|
} else {
|
|
pfxlog.Logger().
|
|
WithField("configId", configId).
|
|
WithField("identityId", identity.Id).
|
|
Error("config not found when adding config references to identity")
|
|
}
|
|
}
|
|
}
|
|
|
|
// HandleServiceEvent will apply the delta event to the router data model. It is not restricted by index calculations.
|
|
// Use ApplyServiceEvent for event logged event handling. This method is generally meant for bulk loading of data
|
|
// during startup.
|
|
func (rdm *RouterDataModel) HandleServiceEvent(index uint64, event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_Service) {
|
|
removeFromConfigs := func(configIds []string) {
|
|
for _, configId := range configIds {
|
|
if config, _ := rdm.Configs.Get(configId); config != nil {
|
|
config.services.Remove(model.Service.Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
addToConfigs := func(configIds []string) {
|
|
for _, configId := range configIds {
|
|
if config, _ := rdm.Configs.Get(configId); config != nil {
|
|
config.services.Set(model.Service.Id, struct{}{})
|
|
} else {
|
|
pfxlog.Logger().
|
|
WithField("configId", configId).
|
|
WithField("serviceId", model.Service.Id).
|
|
Error("config not found when adding config references to service")
|
|
}
|
|
}
|
|
}
|
|
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
var cleanupActions []*edge_ctrl_pb.DataState_Event
|
|
var removedName string
|
|
rdm.Services.RemoveCb(model.Service.Id, func(key string, v *Service, exists bool) bool {
|
|
if exists {
|
|
removedName = v.Name
|
|
removeFromConfigs(v.Configs)
|
|
|
|
v.servicePolicies.IterCb(func(servicePolicyId string, _ struct{}) {
|
|
// If a service is deleted, the service policy changes should come in first, so this should always be a no-op
|
|
if servicePolicy, _ := rdm.ServicePolicies.Get(servicePolicyId); servicePolicy != nil {
|
|
if servicePolicy.Services.Has(model.Service.Id) {
|
|
cleanupActions = append(cleanupActions, &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
IsSynthetic: false,
|
|
Model: &edge_ctrl_pb.DataState_Event_ServicePolicyChange{
|
|
ServicePolicyChange: &edge_ctrl_pb.DataState_ServicePolicyChange{
|
|
PolicyId: servicePolicyId,
|
|
RelatedEntityIds: []string{model.Service.Id},
|
|
RelatedEntityType: edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedService,
|
|
Add: false,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return exists
|
|
})
|
|
|
|
if removedName != "" {
|
|
// Only drop the index entry if it still points at us; a new service with the
|
|
// same name might have been created between our RemoveCb and this step.
|
|
rdm.serviceNameIndex.RemoveCb(removedName, func(key string, v string, exists bool) bool {
|
|
return exists && v == model.Service.Id
|
|
})
|
|
}
|
|
|
|
for _, cleanupAction := range cleanupActions {
|
|
rdm.Handle(index, cleanupAction)
|
|
}
|
|
} else {
|
|
updatedService := &Service{
|
|
Id: model.Service.Id,
|
|
Name: model.Service.Name,
|
|
EncryptionRequired: model.Service.EncryptionRequired,
|
|
Configs: model.Service.Configs,
|
|
index: index,
|
|
}
|
|
|
|
var oldName string
|
|
rdm.Services.Upsert(model.Service.Id, updatedService, func(exist bool, valueInMap *Service, newValue *Service) *Service {
|
|
var configsToRemove []string
|
|
var configsToAdd []string
|
|
|
|
if !exist {
|
|
configsToAdd = newValue.Configs
|
|
} else {
|
|
oldName = valueInMap.Name
|
|
configsToRemove, configsToAdd = diffStringSlices(valueInMap.Configs, newValue.Configs)
|
|
}
|
|
|
|
removeFromConfigs(configsToRemove)
|
|
addToConfigs(configsToAdd)
|
|
|
|
if valueInMap != nil {
|
|
updatedService.servicePolicies = valueInMap.servicePolicies
|
|
} else {
|
|
updatedService.servicePolicies = cmap.New[struct{}]()
|
|
}
|
|
|
|
return newValue
|
|
})
|
|
|
|
// Keep the name -> id index in sync. On rename, only remove the old mapping
|
|
// if it still points at this service (same guard as on delete).
|
|
if oldName != "" && oldName != updatedService.Name {
|
|
rdm.serviceNameIndex.RemoveCb(oldName, func(key string, v string, exists bool) bool {
|
|
return exists && v == updatedService.Id
|
|
})
|
|
}
|
|
rdm.serviceNameIndex.Set(updatedService.Name, updatedService.Id)
|
|
|
|
updatedService.servicePolicies.IterCb(func(servicePolicyId string, _ struct{}) {
|
|
rdm.NotifyServicePolicyServiceChange(servicePolicyId, index)
|
|
})
|
|
}
|
|
}
|
|
|
|
func diffStringSlices(slice1, slice2 []string) (onlyInFirst, onlyInSecond []string) {
|
|
// Sort both slices
|
|
sort.Strings(slice1)
|
|
sort.Strings(slice2)
|
|
|
|
i, j := 0, 0
|
|
|
|
for i < len(slice1) && j < len(slice2) {
|
|
if slice1[i] < slice2[j] {
|
|
// Element only in first slice
|
|
onlyInFirst = append(onlyInFirst, slice1[i])
|
|
i++
|
|
} else if slice1[i] > slice2[j] {
|
|
// Element only in second slice
|
|
onlyInSecond = append(onlyInSecond, slice2[j])
|
|
j++
|
|
} else {
|
|
// Element in both slices, skip
|
|
i++
|
|
j++
|
|
}
|
|
}
|
|
|
|
// Add remaining elements from first slice
|
|
for i < len(slice1) {
|
|
onlyInFirst = append(onlyInFirst, slice1[i])
|
|
i++
|
|
}
|
|
|
|
// Add remaining elements from second slice
|
|
for j < len(slice2) {
|
|
onlyInSecond = append(onlyInSecond, slice2[j])
|
|
j++
|
|
}
|
|
|
|
return onlyInFirst, onlyInSecond
|
|
}
|
|
|
|
// HandleConfigTypeEvent will apply the delta event to the router data model. It is not restricted by index calculations.
|
|
// Use ApplyConfigTypeEvent for event logged event handling. This method is generally meant for bulk loading of data
|
|
// during startup.
|
|
func (rdm *RouterDataModel) HandleConfigTypeEvent(index uint64, event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_ConfigType) {
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
rdm.ConfigTypes.Remove(model.ConfigType.Id)
|
|
} else {
|
|
rdm.ConfigTypes.Set(model.ConfigType.Id, &ConfigType{
|
|
Id: model.ConfigType.Id,
|
|
Name: model.ConfigType.Name,
|
|
Target: model.ConfigType.Target,
|
|
index: index,
|
|
})
|
|
|
|
rdm.Configs.IterCb(func(configId string, config *Config) {
|
|
if config.TypeId == model.ConfigType.Id {
|
|
config.services.IterCb(func(serviceId string, _ struct{}) {
|
|
rdm.NotifyServiceOfConfigChange(serviceId, index)
|
|
})
|
|
config.identities.IterCb(func(identityId string, _ struct{}) {
|
|
rdm.NotifyIdentityChange(identityId, index)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// HandleConfigEvent will apply the delta event to the router data model. It is not restricted by index calculations.
|
|
// Use ApplyConfigEvent for event logged event handling. This method is generally meant for bulk loading of data
|
|
// during startup.
|
|
func (rdm *RouterDataModel) HandleConfigEvent(index uint64, event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_Config) {
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
var removedTypeId string
|
|
rdm.Configs.RemoveCb(model.Config.Id, func(key string, v *Config, exists bool) bool {
|
|
if v != nil {
|
|
removedTypeId = v.TypeId
|
|
v.services.IterCb(func(serviceId string, _ struct{}) {
|
|
rdm.NotifyServiceOfConfigChange(serviceId, index)
|
|
})
|
|
v.identities.IterCb(func(identityId string, _ struct{}) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
for serviceId, configData := range identity.ServiceConfigs {
|
|
for configTypeId, configId := range configData.Configs {
|
|
if configId == v.Id {
|
|
delete(configData.Configs, configTypeId)
|
|
}
|
|
}
|
|
if len(configData.Configs) == 0 {
|
|
delete(identity.ServiceConfigs, serviceId)
|
|
}
|
|
}
|
|
})
|
|
rdm.NotifyIdentityChange(identityId, index)
|
|
})
|
|
}
|
|
return exists
|
|
})
|
|
if removedTypeId != "" {
|
|
rdm.dispatchRouterConfigRemove(removedTypeId)
|
|
}
|
|
} else {
|
|
changed := false
|
|
rdm.Configs.Upsert(model.Config.Id, nil, func(exist bool, valueInMap *Config, newValue *Config) *Config {
|
|
result := &Config{
|
|
Id: model.Config.Id,
|
|
TypeId: model.Config.TypeId,
|
|
Name: model.Config.Name,
|
|
DataJson: model.Config.DataJson,
|
|
services: cmap.New[struct{}](),
|
|
identities: cmap.New[struct{}](),
|
|
index: index,
|
|
}
|
|
|
|
if valueInMap != nil {
|
|
result.services = valueInMap.services
|
|
result.identities = valueInMap.identities
|
|
}
|
|
|
|
if !result.Equals(valueInMap) {
|
|
changed = true
|
|
result.services.IterCb(func(serviceId string, _ struct{}) {
|
|
rdm.NotifyServiceOfConfigChange(serviceId, index)
|
|
})
|
|
|
|
result.identities.IterCb(func(key string, _ struct{}) {
|
|
if identity, _ := rdm.Identities.Get(key); identity != nil {
|
|
identity.NotifyServiceChange(rdm)
|
|
}
|
|
})
|
|
}
|
|
|
|
return result
|
|
})
|
|
if changed {
|
|
rdm.dispatchRouterConfigApply(model.Config.TypeId, model.Config.DataJson)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetRouterConfigSubscriber registers (or clears, when s is nil) the subscriber
|
|
// that receives router-target Config events. Subsequent HandleConfigEvent calls
|
|
// dispatch through s. Replaces any previous subscriber. This sets only the
|
|
// hook; bootstrap and diff-on-resync are handled by the state manager that
|
|
// owns the subscriber lifecycle.
|
|
func (rdm *RouterDataModel) SetRouterConfigSubscriber(s RouterConfigEventSubscriber) {
|
|
if s == nil {
|
|
rdm.routerConfigSubscriber.Store(nil)
|
|
return
|
|
}
|
|
rdm.routerConfigSubscriber.Store(&s)
|
|
}
|
|
|
|
// RouterConfigSubscriber returns the currently-registered subscriber, or nil.
|
|
func (rdm *RouterDataModel) RouterConfigSubscriber() RouterConfigEventSubscriber {
|
|
if p := rdm.routerConfigSubscriber.Load(); p != nil {
|
|
return *p
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dispatchRouterConfigApply notifies the subscriber if the typeId resolves to
|
|
// a router-target ConfigType. No-op when no subscriber is set or the type
|
|
// isn't router-target.
|
|
func (rdm *RouterDataModel) dispatchRouterConfigApply(typeId, data string) {
|
|
sub := rdm.RouterConfigSubscriber()
|
|
if sub == nil {
|
|
return
|
|
}
|
|
ct, ok := rdm.ConfigTypes.Get(typeId)
|
|
if !ok || ct == nil || ct.Target != ConfigTypeTargetRouter {
|
|
return
|
|
}
|
|
sub.OnRouterConfigApplied(ct.Name, data)
|
|
}
|
|
|
|
// dispatchRouterConfigRemove notifies the subscriber if the typeId resolves to
|
|
// a router-target ConfigType. ConfigType records outlive the Config records
|
|
// that reference them, so the lookup succeeds even after the Config has been
|
|
// removed.
|
|
func (rdm *RouterDataModel) dispatchRouterConfigRemove(typeId string) {
|
|
sub := rdm.RouterConfigSubscriber()
|
|
if sub == nil {
|
|
return
|
|
}
|
|
ct, ok := rdm.ConfigTypes.Get(typeId)
|
|
if !ok || ct == nil || ct.Target != ConfigTypeTargetRouter {
|
|
return
|
|
}
|
|
sub.OnRouterConfigRemoved(ct.Name)
|
|
}
|
|
|
|
func (rdm *RouterDataModel) applyUpdateServicePolicyEvent(index uint64, model *edge_ctrl_pb.DataState_Event_ServicePolicy) {
|
|
servicePolicy := model.ServicePolicy
|
|
rdm.ServicePolicies.Upsert(servicePolicy.Id, nil, func(exist bool, valueInMap *ServicePolicy, newValue *ServicePolicy) *ServicePolicy {
|
|
if valueInMap == nil {
|
|
return &ServicePolicy{
|
|
Id: servicePolicy.Id,
|
|
Name: servicePolicy.Name,
|
|
PolicyType: servicePolicy.PolicyType,
|
|
Services: cmap.New[struct{}](),
|
|
PostureChecks: cmap.New[struct{}](),
|
|
Identities: cmap.New[struct{}](),
|
|
servicesIndex: index,
|
|
postureChecksIndex: 0,
|
|
}
|
|
} else {
|
|
updatedValue := &ServicePolicy{
|
|
Id: servicePolicy.Id,
|
|
Name: servicePolicy.Name,
|
|
PolicyType: servicePolicy.PolicyType,
|
|
Services: valueInMap.Services,
|
|
PostureChecks: valueInMap.PostureChecks,
|
|
Identities: valueInMap.Identities,
|
|
servicesIndex: index,
|
|
}
|
|
|
|
if valueInMap.PolicyType != servicePolicy.PolicyType {
|
|
valueInMap.Identities.IterCb(func(key string, _ struct{}) {
|
|
rdm.withLockedIdentity(key, func(identity *Identity) {
|
|
identity.removedFromPolicy(valueInMap, index)
|
|
identity.addedToPolicy(updatedValue, index)
|
|
})
|
|
})
|
|
}
|
|
|
|
return updatedValue
|
|
}
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) applyDeleteServicePolicyEvent(index uint64, model *edge_ctrl_pb.DataState_Event_ServicePolicy) {
|
|
rdm.ServicePolicies.RemoveCb(model.ServicePolicy.Id, func(key string, v *ServicePolicy, exists bool) bool {
|
|
if v != nil {
|
|
v.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
identity.removedFromPolicy(v, index)
|
|
})
|
|
rdm.queueSyncCheck(identityId)
|
|
})
|
|
}
|
|
return exists
|
|
})
|
|
}
|
|
|
|
// HandleServicePolicyEvent will apply the delta event to the router data model. It is not restricted by index calculations.
|
|
// Use ApplyServicePolicyEvent for event logged event handling. This method is generally meant for bulk loading of data
|
|
// during startup.
|
|
func (rdm *RouterDataModel) HandleServicePolicyEvent(index uint64, event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_ServicePolicy) {
|
|
pfxlog.Logger().WithField("policyId", model.ServicePolicy.Id).WithField("action", event.Action).Debug("applying service policy event")
|
|
switch event.Action {
|
|
case edge_ctrl_pb.DataState_Create:
|
|
rdm.applyUpdateServicePolicyEvent(index, model)
|
|
case edge_ctrl_pb.DataState_Update:
|
|
rdm.applyUpdateServicePolicyEvent(index, model)
|
|
case edge_ctrl_pb.DataState_Delete:
|
|
rdm.applyDeleteServicePolicyEvent(index, model)
|
|
}
|
|
}
|
|
|
|
// HandlePostureCheckEvent will apply the delta event to the router data model. It is not restricted by index calculations.
|
|
// Use ApplyPostureCheckEvent for event logged event handling. This method is generally meant for bulk loading of data
|
|
// during startup.
|
|
func (rdm *RouterDataModel) HandlePostureCheckEvent(index uint64, event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_PostureCheck) {
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
var cleanupActions []*edge_ctrl_pb.DataState_Event
|
|
|
|
rdm.withPostureCheck(model.PostureCheck.Id, func(postureCheck *PostureCheck) {
|
|
postureCheck.servicePolicies.IterCb(func(servicePolicyId string, _ struct{}) {
|
|
rdm.withServicePolicy(servicePolicyId, func(servicePolicy *ServicePolicy) {
|
|
if servicePolicy.PostureChecks.Has(model.PostureCheck.Id) {
|
|
cleanupActions = append(cleanupActions, &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_ServicePolicyChange{
|
|
ServicePolicyChange: &edge_ctrl_pb.DataState_ServicePolicyChange{
|
|
PolicyId: servicePolicyId,
|
|
RelatedEntityIds: []string{model.PostureCheck.Id},
|
|
RelatedEntityType: edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedPostureCheck,
|
|
Add: false,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
for _, cleanupAction := range cleanupActions {
|
|
rdm.Handle(index, cleanupAction)
|
|
}
|
|
|
|
rdm.PostureChecks.Remove(model.PostureCheck.Id)
|
|
|
|
} else {
|
|
postureCheck := &PostureCheck{
|
|
DataStatePostureCheck: model.PostureCheck,
|
|
index: index,
|
|
}
|
|
|
|
rdm.PostureChecks.Upsert(model.PostureCheck.Id, nil, func(exist bool, valueInMap *PostureCheck, newValue *PostureCheck) *PostureCheck {
|
|
if valueInMap != nil {
|
|
postureCheck.servicePolicies = valueInMap.servicePolicies
|
|
} else {
|
|
postureCheck.servicePolicies = cmap.New[struct{}]()
|
|
}
|
|
return postureCheck
|
|
})
|
|
|
|
postureCheck.servicePolicies.IterCb(func(servicePolicyId string, _ struct{}) {
|
|
rdm.NotifyServicePolicyPostureChecksChange(servicePolicyId, index)
|
|
})
|
|
}
|
|
}
|
|
|
|
// HandlePublicKeyEvent will apply the delta event to the router data model. It is not restricted by index calculations.
|
|
// Use ApplyPublicKeyEvent for event logged event handling. This method is generally meant for bulk loading of data
|
|
// during startup.
|
|
func (rdm *RouterDataModel) HandlePublicKeyEvent(event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_PublicKey) {
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
rdm.PublicKeys.Remove(model.PublicKey.Kid)
|
|
} else {
|
|
rdm.PublicKeys.Set(model.PublicKey.Kid, model.PublicKey)
|
|
}
|
|
rdm.recalculateCachedPublicKeys()
|
|
}
|
|
|
|
// HandleRevocationEvent applies the given revocation delta event to the router data model.
|
|
func (rdm *RouterDataModel) HandleRevocationEvent(event *edge_ctrl_pb.DataState_Event, model *edge_ctrl_pb.DataState_Event_Revocation) {
|
|
if event.Action == edge_ctrl_pb.DataState_Delete {
|
|
rdm.Revocations.Remove(model.Revocation.Id)
|
|
} else {
|
|
rdm.Revocations.Set(model.Revocation.Id, model.Revocation)
|
|
}
|
|
}
|
|
|
|
// IsApiSessionRevoked reports whether the api-session with the given id has been
|
|
// revoked (e.g. by OIDC end-session). The api-session id is a unique value, so
|
|
// an api-session-typed revocation keyed by it unambiguously targets that
|
|
// session.
|
|
func (rdm *RouterDataModel) IsApiSessionRevoked(apiSessionId string) bool {
|
|
if revocation, found := rdm.Revocations.Get(apiSessionId); found {
|
|
return revocation.Type == RevocationTypeApiSession
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsIdentityRevoked reports whether a session for the given identity, issued at
|
|
// issuedAt, has been revoked (e.g. by disabling/deleting the identity or an
|
|
// identity-scoped revocation). The revocation carries an IssuedBefore cutoff so
|
|
// only sessions that predate it are revoked; a session re-authenticated after
|
|
// the cutoff survives the still-lingering revocation. A revocation with no
|
|
// cutoff is treated as revoking all of the identity's sessions.
|
|
func (rdm *RouterDataModel) IsIdentityRevoked(identityId string, issuedAt time.Time) bool {
|
|
if revocation, found := rdm.Revocations.Get(identityId); found && revocation.Type == RevocationTypeIdentity {
|
|
if revocation.IssuedBefore == nil {
|
|
return true
|
|
}
|
|
return issuedAt.Before(revocation.IssuedBefore.AsTime())
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (rdm *RouterDataModel) HandleServicePolicyChange(index uint64, model *edge_ctrl_pb.DataState_ServicePolicyChange) {
|
|
log := pfxlog.Logger().
|
|
WithField("index", index).
|
|
WithField("rdm", fmt.Sprintf("%p", rdm)).
|
|
WithField("policyId", model.PolicyId).
|
|
WithField("isAdd", model.Add).
|
|
WithField("relatedEntityType", model.RelatedEntityType).
|
|
WithField("relatedEntityIds", model.RelatedEntityIds)
|
|
log.Debug("applying service policy change event")
|
|
|
|
servicePolicy, _ := rdm.ServicePolicies.Get(model.PolicyId)
|
|
|
|
if servicePolicy == nil {
|
|
if model.Add {
|
|
log.Error("service policy not present in router data model")
|
|
}
|
|
return
|
|
}
|
|
|
|
servicesIndex := servicePolicy.servicesIndex
|
|
postureChecksIndex := servicePolicy.postureChecksIndex
|
|
|
|
var additionalIdentitiesToUpdate []string
|
|
|
|
switch model.RelatedEntityType {
|
|
case edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedIdentity:
|
|
for _, identityId := range model.RelatedEntityIds {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
if model.Add {
|
|
servicePolicy.Identities.Set(identityId, struct{}{})
|
|
identity.addedToPolicy(servicePolicy, index)
|
|
} else {
|
|
servicePolicy.Identities.Remove(identityId)
|
|
identity.removedFromPolicy(servicePolicy, index)
|
|
}
|
|
})
|
|
}
|
|
|
|
if !model.Add {
|
|
additionalIdentitiesToUpdate = model.RelatedEntityIds
|
|
}
|
|
|
|
case edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedService:
|
|
servicesIndex = index
|
|
servicePolicy.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
if model.Add {
|
|
identity.servicesAddedToPolicy(servicePolicy, model.RelatedEntityIds, index)
|
|
} else {
|
|
identity.servicesRemovedFromPolicy(servicePolicy, model.RelatedEntityIds, index)
|
|
}
|
|
})
|
|
})
|
|
|
|
startCount := servicePolicy.Services.Count()
|
|
|
|
if model.Add {
|
|
for _, serviceId := range model.RelatedEntityIds {
|
|
servicePolicy.Services.Set(serviceId, struct{}{})
|
|
}
|
|
rdm.withServices(model.RelatedEntityIds, func(service *Service) {
|
|
service.servicePolicies.Set(servicePolicy.Id, struct{}{})
|
|
})
|
|
|
|
if startCount == 0 && servicePolicy.Services.Count() > 0 {
|
|
servicePolicy.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
identity.postureChecksAddedToPolicy(servicePolicy, servicePolicy.PostureChecks.Keys(), index)
|
|
})
|
|
})
|
|
}
|
|
|
|
} else {
|
|
for _, serviceId := range model.RelatedEntityIds {
|
|
servicePolicy.Services.Remove(serviceId)
|
|
}
|
|
rdm.withServices(model.RelatedEntityIds, func(service *Service) {
|
|
service.servicePolicies.Remove(servicePolicy.Id)
|
|
})
|
|
|
|
if startCount > 0 && servicePolicy.Services.Count() == 0 {
|
|
servicePolicy.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
identity.postureChecksRemovedFromPolicy(servicePolicy, servicePolicy.PostureChecks.Keys(), index)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
case edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedPostureCheck:
|
|
postureChecksIndex = index
|
|
|
|
if !servicePolicy.Services.IsEmpty() {
|
|
servicePolicy.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
if model.Add {
|
|
identity.postureChecksAddedToPolicy(servicePolicy, model.RelatedEntityIds, index)
|
|
} else {
|
|
identity.postureChecksRemovedFromPolicy(servicePolicy, model.RelatedEntityIds, index)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
if model.Add {
|
|
for _, postureCheckId := range model.RelatedEntityIds {
|
|
servicePolicy.PostureChecks.Set(postureCheckId, struct{}{})
|
|
}
|
|
rdm.withPostureChecks(model.RelatedEntityIds, func(postureCheck *PostureCheck) {
|
|
postureCheck.servicePolicies.Set(servicePolicy.Id, struct{}{})
|
|
})
|
|
} else {
|
|
for _, postureCheckId := range model.RelatedEntityIds {
|
|
servicePolicy.PostureChecks.Remove(postureCheckId)
|
|
}
|
|
rdm.withPostureChecks(model.RelatedEntityIds, func(postureCheck *PostureCheck) {
|
|
postureCheck.servicePolicies.Remove(servicePolicy.Id)
|
|
})
|
|
}
|
|
}
|
|
|
|
rdm.ServicePolicies.Set(model.PolicyId, &ServicePolicy{
|
|
Id: servicePolicy.Id,
|
|
Name: servicePolicy.Name,
|
|
PolicyType: servicePolicy.PolicyType,
|
|
Services: servicePolicy.Services,
|
|
PostureChecks: servicePolicy.PostureChecks,
|
|
Identities: servicePolicy.Identities,
|
|
servicesIndex: servicesIndex,
|
|
postureChecksIndex: postureChecksIndex,
|
|
})
|
|
|
|
servicePolicy.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
if identity, _ := rdm.Identities.Get(identityId); identity != nil {
|
|
identity.NotifyServiceChange(rdm)
|
|
}
|
|
})
|
|
|
|
for _, identityId := range additionalIdentitiesToUpdate {
|
|
if identity, _ := rdm.Identities.Get(identityId); identity != nil {
|
|
identity.NotifyServiceChange(rdm)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) GetPublicKeys() map[string]crypto.PublicKey {
|
|
return rdm.cachedPublicKeys.Load()
|
|
}
|
|
|
|
func (rdm *RouterDataModel) getPublicKeysAsCmap() cmap.ConcurrentMap[string, crypto.PublicKey] {
|
|
m := cmap.New[crypto.PublicKey]()
|
|
for k, v := range rdm.cachedPublicKeys.Load() {
|
|
m.Set(k, v)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (rdm *RouterDataModel) recalculateCachedPublicKeys() {
|
|
publicKeys := map[string]crypto.PublicKey{}
|
|
rdm.PublicKeys.IterCb(func(kid string, pubKey *edge_ctrl_pb.DataState_PublicKey) {
|
|
log := pfxlog.Logger().WithField("format", pubKey.Format).WithField("kid", kid)
|
|
|
|
switch pubKey.Format {
|
|
case edge_ctrl_pb.DataState_PublicKey_X509CertDer:
|
|
if cert, err := x509.ParseCertificate(pubKey.GetData()); err != nil {
|
|
log.WithError(err).Error("error parsing x509 certificate DER")
|
|
} else {
|
|
publicKeys[kid] = cert.PublicKey
|
|
}
|
|
case edge_ctrl_pb.DataState_PublicKey_PKIXPublicKey:
|
|
if pub, err := x509.ParsePKIXPublicKey(pubKey.GetData()); err != nil {
|
|
log.WithError(err).Error("error parsing PKIX public key DER")
|
|
} else {
|
|
publicKeys[kid] = pub
|
|
}
|
|
default:
|
|
log.Error("unknown public key format")
|
|
}
|
|
})
|
|
rdm.cachedPublicKeys.Store(publicKeys)
|
|
}
|
|
|
|
func (rdm *RouterDataModel) GetDataState() *edge_ctrl_pb.DataState {
|
|
var result *edge_ctrl_pb.DataState
|
|
rdm.WhileLocked(func(currentIndex uint64) {
|
|
result = rdm.getDataStateAlreadyLocked(currentIndex)
|
|
})
|
|
return result
|
|
}
|
|
|
|
func (rdm *RouterDataModel) getDataStateAlreadyLocked(index uint64) *edge_ctrl_pb.DataState {
|
|
var events []*edge_ctrl_pb.DataState_Event
|
|
|
|
rdm.ConfigTypes.IterCb(func(key string, v *ConfigType) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_ConfigType{
|
|
ConfigType: v.ToProtobuf(),
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
})
|
|
|
|
rdm.Configs.IterCb(func(key string, v *Config) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Config{
|
|
Config: v.ToProtobuf(),
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
})
|
|
|
|
servicePolicyIdentities := map[string]*edge_ctrl_pb.DataState_ServicePolicyChange{}
|
|
|
|
rdm.Identities.IterCb(func(key string, v *Identity) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Identity{
|
|
Identity: v.ToProtobuf(),
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
|
|
v.IterateServicePolicies(func(policyId string) {
|
|
change := servicePolicyIdentities[policyId]
|
|
if change == nil {
|
|
change = &edge_ctrl_pb.DataState_ServicePolicyChange{
|
|
PolicyId: policyId,
|
|
RelatedEntityType: edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedIdentity,
|
|
Add: true,
|
|
}
|
|
servicePolicyIdentities[policyId] = change
|
|
}
|
|
change.RelatedEntityIds = append(change.RelatedEntityIds, v.Id)
|
|
})
|
|
})
|
|
|
|
rdm.Services.IterCb(func(key string, v *Service) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Service{
|
|
Service: v.ToProtobuf(),
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
})
|
|
|
|
rdm.Routers.IterCb(func(key string, v *edge_ctrl_pb.DataState_Router) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Router{
|
|
Router: v,
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
})
|
|
|
|
rdm.PostureChecks.IterCb(func(key string, v *PostureCheck) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_PostureCheck{
|
|
PostureCheck: v.DataStatePostureCheck,
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
})
|
|
|
|
rdm.ServicePolicies.IterCb(func(key string, v *ServicePolicy) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_ServicePolicy{
|
|
ServicePolicy: v.ToProtobuf(),
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
|
|
addServicesChange := &edge_ctrl_pb.DataState_ServicePolicyChange{
|
|
PolicyId: v.Id,
|
|
RelatedEntityType: edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedService,
|
|
Add: true,
|
|
}
|
|
v.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
addServicesChange.RelatedEntityIds = append(addServicesChange.RelatedEntityIds, serviceId)
|
|
})
|
|
events = append(events, &edge_ctrl_pb.DataState_Event{
|
|
Model: &edge_ctrl_pb.DataState_Event_ServicePolicyChange{
|
|
ServicePolicyChange: addServicesChange,
|
|
},
|
|
})
|
|
|
|
addPostureCheckChanges := &edge_ctrl_pb.DataState_ServicePolicyChange{
|
|
PolicyId: v.Id,
|
|
RelatedEntityType: edge_ctrl_pb.ServicePolicyRelatedEntityType_RelatedPostureCheck,
|
|
Add: true,
|
|
}
|
|
v.PostureChecks.IterCb(func(postureCheckId string, _ struct{}) {
|
|
addPostureCheckChanges.RelatedEntityIds = append(addPostureCheckChanges.RelatedEntityIds, postureCheckId)
|
|
})
|
|
events = append(events, &edge_ctrl_pb.DataState_Event{
|
|
Model: &edge_ctrl_pb.DataState_Event_ServicePolicyChange{
|
|
ServicePolicyChange: addPostureCheckChanges,
|
|
},
|
|
})
|
|
|
|
if addIdentityChanges, found := servicePolicyIdentities[v.Id]; found {
|
|
events = append(events, &edge_ctrl_pb.DataState_Event{
|
|
Model: &edge_ctrl_pb.DataState_Event_ServicePolicyChange{
|
|
ServicePolicyChange: addIdentityChanges,
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
rdm.PublicKeys.IterCb(func(key string, v *edge_ctrl_pb.DataState_PublicKey) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_PublicKey{
|
|
PublicKey: v,
|
|
},
|
|
IsSynthetic: true,
|
|
}
|
|
events = append(events, newEvent)
|
|
})
|
|
|
|
rdm.Revocations.IterCb(func(key string, v *edge_ctrl_pb.DataState_Revocation) {
|
|
newEvent := &edge_ctrl_pb.DataState_Event{
|
|
Action: edge_ctrl_pb.DataState_Create,
|
|
Model: &edge_ctrl_pb.DataState_Event_Revocation{
|
|
Revocation: v,
|
|
},
|
|
}
|
|
events = append(events, newEvent)
|
|
})
|
|
|
|
var caches map[string]*edge_ctrl_pb.Cache
|
|
|
|
if !rdm.terminatorIdCache.IsEmpty() {
|
|
caches = map[string]*edge_ctrl_pb.Cache{}
|
|
|
|
cache := &edge_ctrl_pb.Cache{
|
|
Data: map[string][]byte{},
|
|
}
|
|
|
|
rdm.terminatorIdCache.IterCb(func(key string, v string) {
|
|
if rdm.Services.Has(key) {
|
|
cache.Data[key] = []byte(v)
|
|
}
|
|
})
|
|
|
|
caches[edge_ctrl_pb.CacheType_TerminatorIds.String()] = cache
|
|
}
|
|
|
|
return &edge_ctrl_pb.DataState{
|
|
Events: events,
|
|
EndIndex: index,
|
|
TimelineId: rdm.timelineId,
|
|
Caches: caches,
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) Save(path string) {
|
|
rdm.WhileLocked(func(index uint64) {
|
|
// nothing to save
|
|
if rdm.lastSaveIndex != nil && *rdm.lastSaveIndex == index {
|
|
pfxlog.Logger().Debug("no changes to router model, nothing to save")
|
|
return
|
|
}
|
|
|
|
state := rdm.getDataStateAlreadyLocked(index)
|
|
stateBytes, err := proto.Marshal(state)
|
|
|
|
if err != nil {
|
|
pfxlog.Logger().WithError(err).Error("could not marshal router data model")
|
|
return
|
|
}
|
|
|
|
// Create a new gzip file
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
pfxlog.Logger().WithError(err).Error("could not marshal router data model, could not create file")
|
|
return
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
// Create a gzip writer
|
|
gz := gzip.NewWriter(file)
|
|
defer func() { _ = gz.Close() }()
|
|
|
|
// Write the gzipped protobuf data to the file
|
|
_, err = gz.Write(stateBytes)
|
|
|
|
if err != nil {
|
|
pfxlog.Logger().WithError(err).Error("could not marshal router data model, could not compress and write")
|
|
return
|
|
}
|
|
|
|
rdm.lastSaveIndex = &index
|
|
})
|
|
}
|
|
|
|
// GetServiceAccessPolicies returns an AccessPolicies instance for an identity attempting to access a service.
|
|
func (rdm *RouterDataModel) GetServiceAccessPolicies(identityId string, serviceId string, policyType edge_ctrl_pb.PolicyType) (*AccessPolicies, error) {
|
|
identity, ok := rdm.Identities.Get(identityId)
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("identity not found by id")
|
|
}
|
|
|
|
service, ok := rdm.Services.Get(serviceId)
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("service not found by id")
|
|
}
|
|
|
|
var policies []*ServicePolicy
|
|
|
|
postureChecks := map[string]*edge_ctrl_pb.DataState_PostureCheck{}
|
|
|
|
identity.IterateServicePolicies(func(servicePolicyId string) {
|
|
servicePolicy, ok := rdm.ServicePolicies.Get(servicePolicyId)
|
|
|
|
if ok && servicePolicy.PolicyType == policyType && servicePolicy.Services.Has(serviceId) {
|
|
policies = append(policies, servicePolicy)
|
|
|
|
servicePolicy.PostureChecks.IterCb(func(postureCheckId string, _ struct{}) {
|
|
if _, ok := postureChecks[postureCheckId]; !ok {
|
|
//ignore ok, if !ok postureCheck == nil which will trigger
|
|
//failure during evaluation
|
|
postureCheck, _ := rdm.PostureChecks.Get(postureCheckId)
|
|
postureChecks[postureCheckId] = postureCheck.DataStatePostureCheck
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
return &AccessPolicies{
|
|
Identity: identity,
|
|
Service: service,
|
|
Policies: policies,
|
|
PostureChecks: postureChecks,
|
|
}, nil
|
|
}
|
|
|
|
func CloneMap[V any](m cmap.ConcurrentMap[string, V]) cmap.ConcurrentMap[string, V] {
|
|
result := cmap.New[V]()
|
|
m.IterCb(func(key string, v V) {
|
|
result.Set(key, v)
|
|
})
|
|
return result
|
|
}
|
|
|
|
func (rdm *RouterDataModel) withIdentity(identityId string, f func(identity *Identity)) {
|
|
if identity, _ := rdm.Identities.Get(identityId); identity != nil {
|
|
f(identity)
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) withLockedIdentity(identityId string, f func(identity *Identity)) {
|
|
if identity, _ := rdm.Identities.Get(identityId); identity != nil {
|
|
identity.lock.Lock()
|
|
defer identity.lock.Unlock()
|
|
f(identity)
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) withServices(serviceIds []string, f func(service *Service)) {
|
|
for _, serviceId := range serviceIds {
|
|
if service, _ := rdm.Services.Get(serviceId); service != nil {
|
|
f(service)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) withService(serviceId string, f func(service *Service)) {
|
|
if service, _ := rdm.Services.Get(serviceId); service != nil {
|
|
f(service)
|
|
}
|
|
}
|
|
|
|
// ServiceIdByName returns the ID of the service with the given name, and a
|
|
// boolean indicating whether an entry was found. Uses an index kept in sync
|
|
// with Services at the mutation sites. Controller enforces unique service
|
|
// names, so this is a 1:1 lookup.
|
|
func (rdm *RouterDataModel) ServiceIdByName(name string) (string, bool) {
|
|
return rdm.serviceNameIndex.Get(name)
|
|
}
|
|
|
|
func (rdm *RouterDataModel) withServicePolicy(servicePolicyId string, f func(servicePolicy *ServicePolicy)) {
|
|
if servicePolicy, _ := rdm.ServicePolicies.Get(servicePolicyId); servicePolicy != nil {
|
|
f(servicePolicy)
|
|
} else {
|
|
pfxlog.Logger().
|
|
WithField("servicePolicyId", servicePolicyId).
|
|
Error("service policy not found when updating model")
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) withPostureChecks(postureCheckIds []string, f func(PostureCheck *PostureCheck)) {
|
|
for _, postureCheckId := range postureCheckIds {
|
|
if postureCheck, _ := rdm.PostureChecks.Get(postureCheckId); postureCheck != nil {
|
|
f(postureCheck)
|
|
} else {
|
|
pfxlog.Logger().
|
|
WithField("postureCheckId", postureCheckId).
|
|
Error("posture check not found when updating model")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) withPostureCheck(postureCheckId string, f func(PostureCheck *PostureCheck)) {
|
|
if postureCheck, _ := rdm.PostureChecks.Get(postureCheckId); postureCheck != nil {
|
|
f(postureCheck)
|
|
} else {
|
|
pfxlog.Logger().
|
|
WithField("postureCheckId", postureCheckId).
|
|
Error("posture check not found when updating model")
|
|
}
|
|
}
|
|
|
|
// SubscribeToIdentityChanges registers subscriber for the identity's change events. When the
|
|
// identity is already in the model, the subscriber is immediately sent an IdentityFullState
|
|
// notification. When it is not yet known (e.g. an SDK connects before the identity has synced to
|
|
// this router), the subscription registers as pending: the identity-created event initializes it
|
|
// and delivers the IdentityFullState notification once the identity arrives (see
|
|
// checkSubsForNewIdentity).
|
|
func (rdm *RouterDataModel) SubscribeToIdentityChanges(identityId string, subscriber IdentityEventSubscriber, isRouterIdentity bool) {
|
|
pfxlog.Logger().WithField("identityId", identityId).Debug("subscribing to changes for identity")
|
|
identity, _ := rdm.Identities.Get(identityId)
|
|
|
|
rdm.EnableServiceAccessTracking(identityId)
|
|
|
|
subscription := rdm.subscriptions.Upsert(identityId, nil, func(exist bool, valueInMap *IdentitySubscription, newValue *IdentitySubscription) *IdentitySubscription {
|
|
if exist {
|
|
valueInMap.listeners.Append(subscriber)
|
|
return valueInMap
|
|
}
|
|
result := &IdentitySubscription{
|
|
IdentityId: identityId,
|
|
IsRouterIdentity: isRouterIdentity,
|
|
}
|
|
result.listeners.Append(subscriber)
|
|
pfxlog.Logger().WithField("identityId", identityId).Debug("added subscription for identity")
|
|
return result
|
|
})
|
|
|
|
if identity == nil {
|
|
// The identity may have arrived between the lookup above and the subscription
|
|
// registration, before queueNewIdentitySubCheck could see the subscription. Re-check so
|
|
// that window cannot leave the subscription uninitialized until the identity next changes.
|
|
identity, _ = rdm.Identities.Get(identityId)
|
|
}
|
|
|
|
if identity != nil {
|
|
state, _ := subscription.initialize(rdm, identity)
|
|
subscriber.NotifyIdentityEvent(state, IdentityFullState)
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) UnsubscribeFromIdentityChanges(identityId string, subscriber IdentityEventSubscriber) {
|
|
removed := rdm.subscriptions.RemoveCb(identityId, func(key string, v *IdentitySubscription, exists bool) bool {
|
|
if v != nil {
|
|
v.listeners.Delete(subscriber)
|
|
if len(v.listeners.Value()) == 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
if removed {
|
|
rdm.DisableServiceAccessTracking(identityId)
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) InheritLocalData(other *RouterDataModel) {
|
|
logger := pfxlog.Logger()
|
|
|
|
trackedCount := 0
|
|
other.Identities.IterCb(func(identityId string, v *Identity) {
|
|
if v.serviceAccessTrackingEnabled.Load() {
|
|
trackedCount++
|
|
rdm.EnableServiceAccessTracking(identityId)
|
|
}
|
|
})
|
|
|
|
subCount := 0
|
|
other.subscriptions.IterCb(func(identityId string, v *IdentitySubscription) {
|
|
subCount++
|
|
rdm.subscriptions.Set(identityId, v)
|
|
// Ensure tracking is enabled for all subscribed identities. The identity iteration
|
|
// above can race with concurrent SubscribeToIdentityChanges calls on the old model,
|
|
// missing identities whose tracking was enabled after their shard was already visited.
|
|
rdm.EnableServiceAccessTracking(identityId)
|
|
})
|
|
|
|
termCacheCount := 0
|
|
other.terminatorIdCache.IterCb(func(key string, v string) {
|
|
termCacheCount++
|
|
rdm.terminatorIdCache.Set(key, v)
|
|
})
|
|
|
|
logger.WithField("trackedIdentities", trackedCount).
|
|
WithField("subscriptions", subCount).
|
|
WithField("terminatorCacheEntries", termCacheCount).
|
|
WithField("newModelIdentities", rdm.Identities.Count()).
|
|
WithField("newModelServices", rdm.Services.Count()).
|
|
WithField("newModelPolicies", rdm.ServicePolicies.Count()).
|
|
Info("inherited local data into new router data model")
|
|
}
|
|
|
|
func (rdm *RouterDataModel) buildServiceListUsingDenormalizedData(sub *IdentitySubscription) (map[string]*IdentityService, map[string]*PostureCheck) {
|
|
log := pfxlog.Logger().WithField("identityId", sub.IdentityId)
|
|
services := map[string]*IdentityService{}
|
|
postureChecks := map[string]*PostureCheck{}
|
|
|
|
rdm.withLockedIdentity(sub.IdentityId, func(identity *Identity) {
|
|
for serviceId, access := range identity.ServiceAccess {
|
|
if service, _ := rdm.Services.Get(serviceId); service != nil {
|
|
identityService := &IdentityService{
|
|
Service: service,
|
|
Configs: map[string]*IdentityConfig{},
|
|
DialAllowed: access.IsDialAllowed(),
|
|
BindAllowed: access.IsBindAllowed(),
|
|
dialPoliciesIndex: access.dialPoliciesIndex,
|
|
bindPoliciesIndex: access.bindPoliciesIndex,
|
|
}
|
|
|
|
services[serviceId] = identityService
|
|
rdm.loadServiceConfigs(sub.Identity, identityService)
|
|
} else {
|
|
log.WithField("serviceId", serviceId).Error("service not found by id")
|
|
}
|
|
}
|
|
|
|
for postureCheckId := range identity.PostureChecks {
|
|
if check, _ := rdm.PostureChecks.Get(postureCheckId); check != nil {
|
|
postureChecks[postureCheckId] = check
|
|
} else {
|
|
log.WithField("postureCheckId", postureCheckId).Error("posture check referenced but not found by id")
|
|
}
|
|
}
|
|
})
|
|
|
|
return services, postureChecks
|
|
}
|
|
|
|
func (rdm *RouterDataModel) buildServiceList(identity *Identity) (map[string]*IdentityService, map[string]*PostureCheck) {
|
|
log := pfxlog.Logger().WithField("identityId", identity.Id)
|
|
services := map[string]*IdentityService{}
|
|
postureChecks := map[string]*PostureCheck{}
|
|
|
|
identity.IterateServicePolicies(func(policyId string) {
|
|
policy, ok := rdm.ServicePolicies.Get(policyId)
|
|
if !ok {
|
|
log.WithField("policyId", policyId).Error("could not find service policy")
|
|
return
|
|
}
|
|
|
|
policy.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
service, ok := rdm.Services.Get(serviceId)
|
|
if !ok {
|
|
log.WithField("policyId", policyId).
|
|
WithField("serviceId", serviceId).
|
|
Error("could not find service")
|
|
return
|
|
}
|
|
|
|
identityService, ok := services[serviceId]
|
|
if !ok {
|
|
identityService = &IdentityService{
|
|
Service: service,
|
|
Configs: map[string]*IdentityConfig{},
|
|
}
|
|
services[serviceId] = identityService
|
|
rdm.loadServiceConfigs(identity, identityService)
|
|
}
|
|
|
|
if policy.IsDial() {
|
|
identityService.DialAllowed = true
|
|
if policy.postureChecksIndex > identityService.dialPoliciesIndex {
|
|
identityService.dialPoliciesIndex = policy.postureChecksIndex
|
|
}
|
|
} else if policy.IsBind() {
|
|
identityService.BindAllowed = true
|
|
if policy.postureChecksIndex > identityService.bindPoliciesIndex {
|
|
identityService.bindPoliciesIndex = policy.postureChecksIndex
|
|
}
|
|
}
|
|
})
|
|
|
|
rdm.loadServicePostureChecks(identity, policy, postureChecks)
|
|
})
|
|
|
|
return services, postureChecks
|
|
}
|
|
|
|
func (rdm *RouterDataModel) buildServiceAccessList(identity *Identity) map[string]*serviceAccess {
|
|
services := map[string]*serviceAccess{}
|
|
|
|
for servicePolicyId := range identity.ServicePolicies {
|
|
if policy, _ := rdm.ServicePolicies.Get(servicePolicyId); policy != nil {
|
|
policy.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
access, ok := services[serviceId]
|
|
if !ok {
|
|
access = &serviceAccess{}
|
|
services[serviceId] = access
|
|
}
|
|
access.updateServicePolicies(policy, max(policy.postureChecksIndex, policy.servicesIndex), true)
|
|
})
|
|
}
|
|
}
|
|
|
|
return services
|
|
}
|
|
|
|
func (rdm *RouterDataModel) loadServicePostureChecks(identity *Identity, policy *ServicePolicy, checks map[string]*PostureCheck) {
|
|
log := pfxlog.Logger().
|
|
WithField("identityId", identity.Id).
|
|
WithField("policyId", policy.Id)
|
|
|
|
if !policy.Services.IsEmpty() {
|
|
policy.PostureChecks.IterCb(func(postureCheckId string, _ struct{}) {
|
|
if _, present := checks[postureCheckId]; !present {
|
|
check, ok := rdm.PostureChecks.Get(postureCheckId)
|
|
if !ok {
|
|
log.WithField("postureCheckId", postureCheckId).Error("could not find posture check")
|
|
} else {
|
|
checks[postureCheckId] = check
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) loadServiceConfigs(identity *Identity, svc *IdentityService) {
|
|
svc.Configs = rdm.GetIdentityServiceConfigs(identity, svc.Service.Id)
|
|
}
|
|
|
|
// GetIdentityServiceConfigs returns the resolved config set for the given identity and service,
|
|
// keyed by config type name. It merges the service's base configs with the identity's per-service
|
|
// config overrides, so the result is the effective config that identity should use for that
|
|
// service. Returns nil if the service is unknown.
|
|
func (rdm *RouterDataModel) GetIdentityServiceConfigs(identity *Identity, serviceId string) map[string]*IdentityConfig {
|
|
svc, ok := rdm.Services.Get(serviceId)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
log := pfxlog.Logger().WithField("identityId", identity.Id).WithField("serviceId", serviceId)
|
|
result := map[string]*IdentityConfig{}
|
|
|
|
for _, configId := range svc.Configs {
|
|
if identityConfig := rdm.loadIdentityConfig(configId, log); identityConfig != nil {
|
|
result[identityConfig.TypeName] = identityConfig
|
|
}
|
|
}
|
|
|
|
if serviceConfigs, hasOverride := identity.ServiceConfigs[serviceId]; hasOverride {
|
|
for _, configId := range serviceConfigs.Configs {
|
|
if identityConfig := rdm.loadIdentityConfig(configId, log); identityConfig != nil {
|
|
result[identityConfig.TypeName] = identityConfig
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (rdm *RouterDataModel) loadIdentityConfig(configId string, log *logrus.Entry) *IdentityConfig {
|
|
config, ok := rdm.Configs.Get(configId)
|
|
if !ok {
|
|
log.WithField("configId", configId).Error("could not find config")
|
|
return nil
|
|
}
|
|
|
|
configType, ok := rdm.ConfigTypes.Get(config.TypeId)
|
|
if !ok {
|
|
log.WithField("configId", configId).
|
|
WithField("configTypeId", config.TypeId).
|
|
Error("could not find config type")
|
|
return nil
|
|
}
|
|
|
|
return &IdentityConfig{
|
|
TypeId: configType.Id,
|
|
TypeName: configType.Name,
|
|
DataJson: config.DataJson,
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) GetEntityCounts() map[string]uint32 {
|
|
result := map[string]uint32{
|
|
"configType": uint32(rdm.ConfigTypes.Count()),
|
|
"configs": uint32(rdm.Configs.Count()),
|
|
"identities": uint32(rdm.Identities.Count()),
|
|
"Services": uint32(rdm.Services.Count()),
|
|
"routers": uint32(rdm.Routers.Count()),
|
|
"service-policies": uint32(rdm.ServicePolicies.Count()),
|
|
"posture-checks": uint32(rdm.PostureChecks.Count()),
|
|
"public-keys": uint32(rdm.PublicKeys.Count()),
|
|
"revocations": uint32(rdm.Revocations.Count()),
|
|
"cached-public-keys": uint32(rdm.getPublicKeysAsCmap().Count()),
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (rdm *RouterDataModel) GetTerminatorIdCache() cmap.ConcurrentMap[string, string] {
|
|
return rdm.terminatorIdCache
|
|
}
|
|
|
|
func (rdm *RouterDataModel) queueSyncCheck(identityId string) {
|
|
if rdm.subscriptions.Has(identityId) {
|
|
rdm.markIdentityForCheck(identityId, IdentityUpdated)
|
|
select {
|
|
case rdm.events <- checkForIdentityChangesEvent{identityId: identityId}:
|
|
default:
|
|
select {
|
|
case rdm.scanSubsNotify <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) queueNewIdentitySubCheck(identityId string) {
|
|
if rdm.subscriptions.Has(identityId) {
|
|
rdm.markIdentityForCheck(identityId, IdentityAdded)
|
|
select {
|
|
case rdm.events <- identityCreatedEvent{identityId: identityId}:
|
|
default:
|
|
select {
|
|
case rdm.scanSubsNotify <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) queueIdentityDeletedSubCheck(identityId string) {
|
|
if rdm.subscriptions.Has(identityId) {
|
|
rdm.markIdentityForCheck(identityId, IdentityDeleted)
|
|
select {
|
|
case rdm.events <- identityDeletedEvent{identityId: identityId}:
|
|
default:
|
|
select {
|
|
case rdm.scanSubsNotify <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) syncSubscriptionIfRequired(identityId string, clearUpdateEntry bool) {
|
|
if subscription, _ := rdm.subscriptions.Get(identityId); subscription != nil {
|
|
requiresSync := true
|
|
|
|
if clearUpdateEntry {
|
|
requiresSync = rdm.markIdentityCheckComplete(identityId, IdentityUpdated)
|
|
}
|
|
|
|
if requiresSync {
|
|
subscription.checkForChanges(rdm)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) checkSubsForNewIdentity(identityId string, clearUpdateEntry bool) {
|
|
pfxlog.Logger().
|
|
WithField("subs", rdm.subscriptions.Count()).
|
|
WithField("identityId", identityId).
|
|
Debug("handling identity created event")
|
|
|
|
requiresSync := true
|
|
|
|
if clearUpdateEntry {
|
|
requiresSync = rdm.markIdentityCheckComplete(identityId, IdentityAdded)
|
|
}
|
|
|
|
if sub, found := rdm.subscriptions.Get(identityId); found {
|
|
if requiresSync {
|
|
identity, _ := rdm.Identities.Get(identityId)
|
|
if identity == nil {
|
|
pfxlog.Logger().WithField("identityId", identityId).Warn("identity not found while checking for subscription initialization")
|
|
return
|
|
}
|
|
state, wasInitialized := sub.initialize(rdm, identity)
|
|
if wasInitialized {
|
|
sub.checkForChanges(rdm)
|
|
} else {
|
|
for _, subscriber := range sub.listeners.Value() {
|
|
subscriber.NotifyIdentityEvent(state, IdentityFullState)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) checkSubsForDeletedIdentity(identityId string, clearUpdateEntry bool) {
|
|
if clearUpdateEntry {
|
|
rdm.markIdentityCheckComplete(identityId, IdentityDeleted)
|
|
}
|
|
|
|
if sub, found := rdm.subscriptions.Get(identityId); found {
|
|
sub.identityRemoved()
|
|
if !sub.IsRouterIdentity {
|
|
rdm.subscriptions.Remove(identityId)
|
|
pfxlog.Logger().WithField("identityId", identityId).Debug("removed subscription for identity")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rdm *RouterDataModel) NotifyServiceOfConfigChange(serviceId string, index uint64) {
|
|
rdm.withService(serviceId, func(service *Service) {
|
|
if service.index < index {
|
|
rdm.Services.Set(serviceId, &Service{
|
|
Id: service.Id,
|
|
Name: service.Name,
|
|
EncryptionRequired: service.EncryptionRequired,
|
|
Configs: service.Configs,
|
|
servicePolicies: service.servicePolicies,
|
|
index: index,
|
|
})
|
|
}
|
|
|
|
service.servicePolicies.IterCb(func(servicePolicyId string, _ struct{}) {
|
|
rdm.NotifyServicePolicyServiceChange(servicePolicyId, index)
|
|
})
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) NotifyIdentityChange(identityId string, _ uint64) {
|
|
rdm.withIdentity(identityId, func(identity *Identity) {
|
|
identity.NotifyServiceChange(rdm)
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) NotifyServicePolicyServiceChange(servicePolicyId string, serviceIndex uint64) {
|
|
rdm.withServicePolicy(servicePolicyId, func(servicePolicy *ServicePolicy) {
|
|
if servicePolicy.servicesIndex < serviceIndex {
|
|
rdm.ServicePolicies.Set(servicePolicyId, &ServicePolicy{
|
|
Id: servicePolicy.Id,
|
|
Name: servicePolicy.Name,
|
|
PolicyType: servicePolicy.PolicyType,
|
|
Services: servicePolicy.Services,
|
|
PostureChecks: servicePolicy.PostureChecks,
|
|
Identities: servicePolicy.Identities,
|
|
servicesIndex: serviceIndex,
|
|
postureChecksIndex: servicePolicy.postureChecksIndex,
|
|
})
|
|
}
|
|
|
|
servicePolicy.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
if identity, _ := rdm.Identities.Get(identityId); identity != nil {
|
|
identity.NotifyServiceChange(rdm)
|
|
} else {
|
|
pfxlog.Logger().
|
|
WithField("identityId", identityId).
|
|
WithField("servicePolicyId", servicePolicyId).
|
|
Error("identity not found when updating service policy")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) NotifyServicePolicyPostureChecksChange(servicePolicyId string, postureChecksIndex uint64) {
|
|
rdm.withServicePolicy(servicePolicyId, func(servicePolicy *ServicePolicy) {
|
|
if servicePolicy.postureChecksIndex < postureChecksIndex {
|
|
rdm.ServicePolicies.Set(servicePolicyId, &ServicePolicy{
|
|
Id: servicePolicy.Id,
|
|
Name: servicePolicy.Name,
|
|
PolicyType: servicePolicy.PolicyType,
|
|
Services: servicePolicy.Services,
|
|
PostureChecks: servicePolicy.PostureChecks,
|
|
Identities: servicePolicy.Identities,
|
|
servicesIndex: servicePolicy.servicesIndex,
|
|
postureChecksIndex: postureChecksIndex,
|
|
})
|
|
}
|
|
servicePolicy.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
if identity, _ := rdm.Identities.Get(identityId); identity != nil {
|
|
identity.NotifyServiceChange(rdm)
|
|
} else {
|
|
pfxlog.Logger().
|
|
WithField("identityId", identityId).
|
|
WithField("servicePolicyId", servicePolicyId).
|
|
Error("identity not found when updating service policy")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) EnableServiceAccessTracking(identityId string) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
if identity.serviceAccessTrackingEnabled.CompareAndSwap(false, true) {
|
|
identity.ServiceAccess = rdm.buildServiceAccessList(identity)
|
|
pfxlog.Logger().
|
|
WithField("identityId", identityId).
|
|
WithField("servicePolicyCount", len(identity.ServicePolicies)).
|
|
WithField("serviceAccessCount", len(identity.ServiceAccess)).
|
|
Info("enabled service access tracking")
|
|
}
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) DisableServiceAccessTracking(identityId string) {
|
|
rdm.withLockedIdentity(identityId, func(identity *Identity) {
|
|
if identity.serviceAccessTrackingEnabled.CompareAndSwap(true, false) {
|
|
identity.ServiceAccess = map[string]*serviceAccess{}
|
|
}
|
|
})
|
|
}
|
|
|
|
type DiffType string
|
|
|
|
const (
|
|
DiffTypeAdd = "added"
|
|
DiffTypeMod = "modified"
|
|
DiffTypeSub = "removed"
|
|
)
|
|
|
|
type DiffSink func(entityType string, id string, diffType DiffType, detail string)
|
|
|
|
func (rdm *RouterDataModel) Validate(correct *RouterDataModel, sink DiffSink) {
|
|
correct.Diff(rdm, sink)
|
|
rdm.subscriptions.IterCb(func(key string, v *IdentitySubscription) {
|
|
v.Diff(rdm, false, sink)
|
|
})
|
|
rdm.validateServiceNameIndex(sink)
|
|
}
|
|
|
|
// validateServiceNameIndex reports any inconsistency between Services and the
|
|
// private serviceNameIndex. Each service must have a name -> id entry that
|
|
// points back at it, and each index entry must reference an existing service
|
|
// whose Name equals the indexed key.
|
|
func (rdm *RouterDataModel) validateServiceNameIndex(sink DiffSink) {
|
|
const et = "service-name-index"
|
|
|
|
rdm.Services.IterCb(func(id string, svc *Service) {
|
|
indexed, ok := rdm.serviceNameIndex.Get(svc.Name)
|
|
if !ok {
|
|
sink(et, svc.Name, DiffTypeSub, fmt.Sprintf("missing index entry for service %q (id=%s)", svc.Name, id))
|
|
} else if indexed != id {
|
|
sink(et, svc.Name, DiffTypeMod, fmt.Sprintf("index entry for %q points at %s, expected %s", svc.Name, indexed, id))
|
|
}
|
|
})
|
|
|
|
rdm.serviceNameIndex.IterCb(func(name, id string) {
|
|
svc, ok := rdm.Services.Get(id)
|
|
if !ok {
|
|
sink(et, name, DiffTypeAdd, fmt.Sprintf("index entry %q -> %s but no such service", name, id))
|
|
} else if svc.Name != name {
|
|
sink(et, name, DiffTypeMod, fmt.Sprintf("index key %q maps to service %s but service name is %q", name, id, svc.Name))
|
|
}
|
|
})
|
|
}
|
|
|
|
func (rdm *RouterDataModel) Diff(o *RouterDataModel, sink DiffSink) {
|
|
if o == nil {
|
|
sink("router-data-model", "root", DiffTypeSub, "router data model not present")
|
|
return
|
|
}
|
|
|
|
rdm.PublicKeys.IterCb(func(key string, v *edge_ctrl_pb.DataState_PublicKey) {
|
|
sort.Slice(v.Usages, func(i, j int) bool {
|
|
return v.Usages[i] < v.Usages[j]
|
|
})
|
|
})
|
|
|
|
o.PublicKeys.IterCb(func(key string, v *edge_ctrl_pb.DataState_PublicKey) {
|
|
sort.Slice(v.Usages, func(i, j int) bool {
|
|
return v.Usages[i] < v.Usages[j]
|
|
})
|
|
})
|
|
|
|
diffType("configType", rdm.ConfigTypes, o.ConfigTypes, sink, ConfigType{}, DataStateConfigType{})
|
|
diffType("config", rdm.Configs, o.Configs, sink, Config{}, DataStateConfig{})
|
|
diffType("identity", rdm.Identities, o.Identities, sink, Identity{}, DataStateIdentity{}, serviceAccess{}, edge_ctrl_pb.DataState_ServiceConfigs{})
|
|
diffType("service", rdm.Services, o.Services, sink, Service{}, DataStateService{})
|
|
diffType("router", rdm.Routers, o.Routers, sink, edge_ctrl_pb.DataState_Router{})
|
|
diffType("service-policy", rdm.ServicePolicies, o.ServicePolicies, sink, ServicePolicy{}, DataStateServicePolicy{})
|
|
diffType("posture-check", rdm.PostureChecks, o.PostureChecks, sink,
|
|
PostureCheck{}, DataStatePostureCheck{},
|
|
edge_ctrl_pb.DataState_PostureCheck_Domains_{}, edge_ctrl_pb.DataState_PostureCheck_Domains{},
|
|
edge_ctrl_pb.DataState_PostureCheck_Mac_{}, edge_ctrl_pb.DataState_PostureCheck_Mac{},
|
|
edge_ctrl_pb.DataState_PostureCheck_Mfa_{}, edge_ctrl_pb.DataState_PostureCheck_Mfa{},
|
|
edge_ctrl_pb.DataState_PostureCheck_OsList_{}, edge_ctrl_pb.DataState_PostureCheck_OsList{}, edge_ctrl_pb.DataState_PostureCheck_Os{},
|
|
edge_ctrl_pb.DataState_PostureCheck_Process_{}, edge_ctrl_pb.DataState_PostureCheck_Process{},
|
|
edge_ctrl_pb.DataState_PostureCheck_ProcessMulti_{}, edge_ctrl_pb.DataState_PostureCheck_ProcessMulti{})
|
|
diffType("public-keys", rdm.PublicKeys, o.PublicKeys, sink, edge_ctrl_pb.DataState_PublicKey{})
|
|
diffType("revocations", rdm.Revocations, o.Revocations, sink, edge_ctrl_pb.DataState_Revocation{}, timestamppb.Timestamp{})
|
|
diffMaps("cached-public-keys", rdm.getPublicKeysAsCmap(), o.getPublicKeysAsCmap(), sink, func(a, b crypto.PublicKey) []string {
|
|
if a == nil || b == nil {
|
|
return []string{fmt.Sprintf("cached public key is nil: orig: %v, dest: %v", a, b)}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
type diffF[T any] func(a, b T) []string
|
|
|
|
func diffMaps[T any](entityType string, m1, m2 cmap.ConcurrentMap[string, T], sink DiffSink, differ diffF[T]) {
|
|
hasMissing := false
|
|
m1.IterCb(func(key string, v T) {
|
|
v2, exists := m2.Get(key)
|
|
if !exists {
|
|
sink(entityType, key, DiffTypeSub, "entity missing")
|
|
hasMissing = true
|
|
} else {
|
|
for _, diff := range differ(v, v2) {
|
|
sink(entityType, key, DiffTypeMod, diff)
|
|
}
|
|
}
|
|
})
|
|
|
|
if m1.Count() != m2.Count() || hasMissing {
|
|
m2.IterCb(func(key string, v2 T) {
|
|
if _, exists := m1.Get(key); !exists {
|
|
sink(entityType, key, DiffTypeAdd, "entity unexpected")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func diffType[P any, T *P](entityType string, m1 cmap.ConcurrentMap[string, T], m2 cmap.ConcurrentMap[string, T], sink DiffSink, ignoreTypes ...any) {
|
|
diffReporter := &compareReporter{
|
|
f: func(key string, detail string) {
|
|
sink(entityType, key, DiffTypeMod, detail)
|
|
},
|
|
}
|
|
|
|
hasMissing := false
|
|
adapter := cmp.Reporter(diffReporter)
|
|
syncSetT := cmp.Transformer("CMapSetToMap", func(s cmap.ConcurrentMap[string, struct{}]) map[string]struct{} {
|
|
return CMapToMap(s)
|
|
})
|
|
m1.IterCb(func(key string, v T) {
|
|
v2, exists := m2.Get(key)
|
|
if !exists {
|
|
sink(entityType, key, DiffTypeSub, "entity missing")
|
|
hasMissing = true
|
|
} else {
|
|
diffReporter.key = key
|
|
cmp.Diff(v, v2, syncSetT, cmpopts.IgnoreUnexported(ignoreTypes...), adapter)
|
|
}
|
|
})
|
|
|
|
if m1.Count() != m2.Count() || hasMissing {
|
|
m2.IterCb(func(key string, v2 T) {
|
|
if _, exists := m1.Get(key); !exists {
|
|
sink(entityType, key, DiffTypeAdd, "entity unexpected")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func CMapToMap[T any](m cmap.ConcurrentMap[string, T]) map[string]T {
|
|
result := map[string]T{}
|
|
m.IterCb(func(key string, val T) {
|
|
result[key] = val
|
|
})
|
|
return result
|
|
}
|
|
|
|
type compareReporter struct {
|
|
steps []cmp.PathStep
|
|
key string
|
|
f func(key string, detail string)
|
|
}
|
|
|
|
func (self *compareReporter) PushStep(step cmp.PathStep) {
|
|
self.steps = append(self.steps, step)
|
|
}
|
|
|
|
func (self *compareReporter) Report(result cmp.Result) {
|
|
if !result.Equal() {
|
|
var step cmp.PathStep
|
|
path := &bytes.Buffer{}
|
|
for _, v := range self.steps {
|
|
path.Write([]byte(v.String()))
|
|
step = v
|
|
}
|
|
if step != nil {
|
|
vx, vy := step.Values()
|
|
var x any
|
|
var y any
|
|
|
|
if vx.IsValid() {
|
|
x = vx.Interface()
|
|
}
|
|
if vy.IsValid() {
|
|
y = vy.Interface()
|
|
}
|
|
err := fmt.Sprintf("%s mismatch. orig: %v, copy: %v", path.String(), x, y)
|
|
self.f(self.key, err)
|
|
} else {
|
|
self.f(self.key, "programming error, empty path stack")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *compareReporter) PopStep() {
|
|
self.steps = self.steps[:len(self.steps)-1]
|
|
}
|
|
|
|
// ToMap returns a map representation of the RouterDataModel with recursively exported data
|
|
func (rdm *RouterDataModel) ToMap() map[string]interface{} {
|
|
result := make(map[string]interface{})
|
|
|
|
// Export config types
|
|
configTypes := make(map[string]interface{})
|
|
rdm.ConfigTypes.IterCb(func(key string, v *ConfigType) {
|
|
configTypes[key] = map[string]interface{}{
|
|
"id": v.Id,
|
|
"name": v.Name,
|
|
"index": v.index,
|
|
}
|
|
})
|
|
result["configTypes"] = configTypes
|
|
|
|
// Export configs
|
|
configs := make(map[string]interface{})
|
|
rdm.Configs.IterCb(func(key string, v *Config) {
|
|
services := make([]string, 0)
|
|
v.services.IterCb(func(serviceId string, _ struct{}) {
|
|
services = append(services, serviceId)
|
|
})
|
|
configs[key] = map[string]interface{}{
|
|
"id": v.Id,
|
|
"name": v.Name,
|
|
"typeId": v.TypeId,
|
|
"dataJson": v.DataJson,
|
|
"services": services,
|
|
"index": v.index,
|
|
}
|
|
})
|
|
result["configs"] = configs
|
|
|
|
// Export identities
|
|
identities := make(map[string]interface{})
|
|
rdm.Identities.IterCb(func(key string, v *Identity) {
|
|
servicePolicies := make([]string, 0)
|
|
v.IterateServicePolicies(func(servicePolicyId string) {
|
|
servicePolicies = append(servicePolicies, servicePolicyId)
|
|
})
|
|
|
|
services := make(map[string]interface{})
|
|
v.lock.Lock()
|
|
for serviceId, access := range v.ServiceAccess {
|
|
services[serviceId] = map[string]interface{}{
|
|
"dial": access.DialPoliciesCount,
|
|
"bind": access.BindPoliciesCount,
|
|
}
|
|
}
|
|
|
|
postureChecks := make(map[string]int32)
|
|
for checkId, count := range v.PostureChecks {
|
|
postureChecks[checkId] = count
|
|
}
|
|
v.lock.Unlock()
|
|
|
|
identities[key] = map[string]interface{}{
|
|
"id": v.Id,
|
|
"name": v.Name,
|
|
"disabled": v.Disabled,
|
|
"appDataJson": string(v.AppDataJson),
|
|
"defaultHostingPrecedence": v.DefaultHostingPrecedence,
|
|
"defaultHostingCost": v.DefaultHostingCost,
|
|
"serviceAccessTrackingEnabled": v.serviceAccessTrackingEnabled.Load(),
|
|
"serviceHostingPrecedences": v.ServiceHostingPrecedences,
|
|
"serviceHostingCosts": v.ServiceHostingCosts,
|
|
"serviceConfigs": v.ServiceConfigs,
|
|
"servicePolicies": servicePolicies,
|
|
"services": services,
|
|
"postureChecks": postureChecks,
|
|
"identityIndex": v.identityIndex,
|
|
}
|
|
})
|
|
result["identities"] = identities
|
|
|
|
// Export services
|
|
services := make(map[string]interface{})
|
|
rdm.Services.IterCb(func(key string, v *Service) {
|
|
servicePolicies := make([]string, 0)
|
|
v.servicePolicies.IterCb(func(policyId string, _ struct{}) {
|
|
servicePolicies = append(servicePolicies, policyId)
|
|
})
|
|
|
|
services[key] = map[string]interface{}{
|
|
"id": v.Id,
|
|
"name": v.Name,
|
|
"encryptionRequired": v.EncryptionRequired,
|
|
"configs": v.Configs,
|
|
"servicePolicies": servicePolicies,
|
|
"index": v.index,
|
|
}
|
|
})
|
|
result["services"] = services
|
|
|
|
// Export routers
|
|
routers := make(map[string]interface{})
|
|
rdm.Routers.IterCb(func(key string, v *edge_ctrl_pb.DataState_Router) {
|
|
routers[key] = map[string]interface{}{
|
|
"id": v.Id,
|
|
"name": v.Name,
|
|
"fingerprint": v.Fingerprint,
|
|
"configs": v.Configs,
|
|
"disabled": v.Disabled,
|
|
}
|
|
})
|
|
result["routers"] = routers
|
|
|
|
// Export service policies
|
|
servicePolicies := make(map[string]interface{})
|
|
rdm.ServicePolicies.IterCb(func(key string, v *ServicePolicy) {
|
|
policyServices := make([]string, 0)
|
|
v.Services.IterCb(func(serviceId string, _ struct{}) {
|
|
policyServices = append(policyServices, serviceId)
|
|
})
|
|
|
|
postureChecks := make([]string, 0)
|
|
v.PostureChecks.IterCb(func(checkId string, _ struct{}) {
|
|
postureChecks = append(postureChecks, checkId)
|
|
})
|
|
|
|
identitiesList := make([]string, 0)
|
|
v.Identities.IterCb(func(identityId string, _ struct{}) {
|
|
identitiesList = append(identitiesList, identityId)
|
|
})
|
|
|
|
servicePolicies[key] = map[string]interface{}{
|
|
"id": v.Id,
|
|
"name": v.Name,
|
|
"policyType": v.PolicyType.String(),
|
|
"services": policyServices,
|
|
"postureChecks": postureChecks,
|
|
"identities": identitiesList,
|
|
"index": v.servicesIndex,
|
|
}
|
|
})
|
|
result["servicePolicies"] = servicePolicies
|
|
|
|
// Export posture checks
|
|
postureChecks := make(map[string]interface{})
|
|
rdm.PostureChecks.IterCb(func(key string, v *PostureCheck) {
|
|
postureChecks[key] = map[string]interface{}{
|
|
"id": v.Id,
|
|
"name": v.Name,
|
|
"typeId": v.TypeId,
|
|
"index": v.index,
|
|
"servicePolicies": v.servicePolicies.Keys(),
|
|
}
|
|
})
|
|
result["postureChecks"] = postureChecks
|
|
|
|
// Export subscriptions
|
|
subscriptions := make(map[string]interface{})
|
|
rdm.subscriptions.IterCb(func(key string, v *IdentitySubscription) {
|
|
v.Lock()
|
|
defer v.Unlock()
|
|
|
|
// Export services within subscription
|
|
subServices := make(map[string]interface{})
|
|
for serviceId, identityService := range v.Services {
|
|
var dialPoliciesCount int32
|
|
var bindPoliciesCount int32
|
|
|
|
if v.Identity != nil {
|
|
v.Identity.WithLock(func() {
|
|
if access := v.Identity.ServiceAccess[serviceId]; access != nil {
|
|
dialPoliciesCount = access.DialPoliciesCount
|
|
bindPoliciesCount = access.BindPoliciesCount
|
|
}
|
|
})
|
|
}
|
|
|
|
configsMap := make(map[string]interface{})
|
|
for configTypeName, identityConfig := range identityService.Configs {
|
|
configsMap[configTypeName] = map[string]interface{}{
|
|
"configTypeId": identityConfig.TypeId,
|
|
"configTypeName": identityConfig.TypeName,
|
|
"dataJson": identityConfig.DataJson,
|
|
}
|
|
}
|
|
|
|
subServices[serviceId] = map[string]interface{}{
|
|
"serviceId": identityService.Service.Id,
|
|
"serviceName": identityService.Service.Name,
|
|
"dialAllowed": identityService.IsDialAllowed(),
|
|
"bindAllowed": identityService.IsBindAllowed(),
|
|
"dialPoliciesCount": dialPoliciesCount,
|
|
"bindPoliciesCount": bindPoliciesCount,
|
|
"configs": configsMap,
|
|
}
|
|
}
|
|
|
|
// Export posture checks within subscription
|
|
subChecks := make(map[string]interface{})
|
|
for checkId, check := range v.Checks {
|
|
subChecks[checkId] = map[string]interface{}{
|
|
"id": check.Id,
|
|
"name": check.Name,
|
|
"typeId": check.TypeId,
|
|
}
|
|
}
|
|
|
|
serviceAccessTrackingEnabled := false
|
|
if v.Identity != nil {
|
|
serviceAccessTrackingEnabled = v.Identity.serviceAccessTrackingEnabled.Load()
|
|
}
|
|
|
|
var identityMap map[string]interface{}
|
|
|
|
if v.Identity != nil {
|
|
identityMap = map[string]interface{}{
|
|
"id": v.Identity.Id,
|
|
"name": v.Identity.Name,
|
|
"disabled": v.Identity.Disabled,
|
|
"appDataJson": string(v.Identity.AppDataJson),
|
|
"identityIndex": v.Identity.identityIndex,
|
|
}
|
|
}
|
|
|
|
subscriptions[key] = map[string]interface{}{
|
|
"identityId": v.IdentityId,
|
|
"identity": identityMap,
|
|
"isRecreatable": v.IsRouterIdentity,
|
|
"services": subServices,
|
|
"postureChecks": subChecks,
|
|
"serviceAccessTrackingEnabled": serviceAccessTrackingEnabled,
|
|
}
|
|
})
|
|
result["subscriptions"] = subscriptions
|
|
|
|
return result
|
|
}
|