mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 08:45:41 +00:00
473 lines
13 KiB
Go
473 lines
13 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package model
|
|
|
|
import (
|
|
"bytes"
|
|
"regexp"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jinzhu/copier"
|
|
"github.com/kataras/go-events"
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/ziti/v2/controller/storage/ast"
|
|
"github.com/openziti/ziti/v2/controller/storage/boltz"
|
|
"github.com/openziti/ziti/v2/controller/change"
|
|
"github.com/openziti/ziti/v2/controller/db"
|
|
cmap "github.com/orcaman/concurrent-map/v2"
|
|
"go.etcd.io/bbolt"
|
|
)
|
|
|
|
const (
|
|
EventIdentityPostureDataAltered = "EventIdentityPostureDataAltered"
|
|
)
|
|
|
|
type PostureCache struct {
|
|
identityToPostureData cmap.ConcurrentMap[string, *PostureData]
|
|
apiSessionIdToIdentityId cmap.ConcurrentMap[string, string]
|
|
ticker *time.Ticker
|
|
isRunning atomic.Bool
|
|
events.EventEmmiter
|
|
env Env
|
|
}
|
|
|
|
func newPostureCache(env Env) *PostureCache {
|
|
pc := &PostureCache{
|
|
identityToPostureData: cmap.New[*PostureData](),
|
|
apiSessionIdToIdentityId: cmap.New[string](),
|
|
ticker: time.NewTicker(5 * time.Second),
|
|
EventEmmiter: events.New(),
|
|
env: env,
|
|
}
|
|
|
|
if !env.GetConfig().Edge.DisablePostureChecks {
|
|
pc.run(env.GetCloseNotifyChannel())
|
|
|
|
env.GetStores().ApiSession.AddEntityEventListenerF(pc.ApiSessionCreated, boltz.EntityCreatedAsync)
|
|
env.GetStores().ApiSession.AddEntityEventListenerF(pc.ApiSessionDeleted, boltz.EntityDeletedAsync)
|
|
env.GetStores().Identity.AddEntityEventListenerF(pc.IdentityDeleted, boltz.EntityDeletedAsync)
|
|
|
|
env.GetStores().PostureCheckType.AddListener(pc.PostureCheckChanged, boltz.EntityCreatedAsync, boltz.EntityUpdatedAsync)
|
|
}
|
|
|
|
return pc
|
|
}
|
|
|
|
func (pc *PostureCache) run(closeNotify <-chan struct{}) {
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-pc.ticker.C:
|
|
go pc.evaluate()
|
|
case <-closeNotify:
|
|
pc.ticker.Stop()
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (pc *PostureCache) evaluate() {
|
|
if !pc.isRunning.CompareAndSwap(false, true) {
|
|
return
|
|
}
|
|
|
|
log := pfxlog.Logger()
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("error during posture timeout enforcement: %v", r)
|
|
}
|
|
|
|
pc.isRunning.Store(false)
|
|
}()
|
|
|
|
var lastId []byte
|
|
const maxScanPerTx = 1000
|
|
var toDeleteSessionIds []string
|
|
|
|
newIdentityServiceUpdates := map[string]struct{}{} //tracks the current loops identityIds that have had updates
|
|
completedIdentityServiceUpdates := map[string]struct{}{} //tracks overall identityIds that have been notified of an update
|
|
|
|
done := false
|
|
|
|
// Chunk data in maxToDelete bunches to limit how many sessions we are deleting in a transaction.
|
|
// Requires tracking of which session was last evaluated, kept in lastId.
|
|
for !done {
|
|
var sessions []*db.Session
|
|
_ = pc.env.GetDb().View(func(tx *bbolt.Tx) error {
|
|
cursor := pc.env.GetStores().Session.IterateIds(tx, ast.BoolNodeTrue)
|
|
|
|
if len(lastId) != 0 {
|
|
cursor.Seek(lastId)
|
|
|
|
if cursor.IsValid() {
|
|
if bytes.Equal(cursor.Current(), lastId) {
|
|
cursor.Next()
|
|
}
|
|
}
|
|
}
|
|
|
|
for cursor.IsValid() && len(sessions) < maxScanPerTx {
|
|
if session, _ := pc.env.GetStores().Session.LoadById(tx, string(cursor.Current())); session != nil {
|
|
sessions = append(sessions, session)
|
|
}
|
|
lastId = cursor.Current()
|
|
cursor.Next()
|
|
}
|
|
|
|
if !cursor.IsValid() {
|
|
done = true
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
for _, session := range sessions {
|
|
result := pc.env.GetManagers().Session.EvaluatePostureForService(session.IdentityId, session.ApiSessionId, session.Type, session.ServiceId, "")
|
|
|
|
if !result.Passed {
|
|
log.WithFields(map[string]interface{}{
|
|
"apiSessionId": session.ApiSessionId,
|
|
"identityId": session.IdentityId,
|
|
"sessionId": session.Id,
|
|
}).Tracef("session [%s] failed posture checks, removing", session.Id)
|
|
|
|
toDeleteSessionIds = append(toDeleteSessionIds, session.Id)
|
|
newIdentityServiceUpdates[session.IdentityId] = struct{}{}
|
|
}
|
|
}
|
|
|
|
//delete sessions that failed pc checks, clear list
|
|
for _, sessionId := range toDeleteSessionIds {
|
|
err := pc.env.GetManagers().Session.Delete(sessionId, change.New().SetSourceType("posture.cache").SetChangeAuthorType(change.AuthorTypeController))
|
|
if err != nil {
|
|
log.WithError(err).Errorf("error removing session [%s] due to posture check failure, delete error: %v", sessionId, err)
|
|
}
|
|
}
|
|
toDeleteSessionIds = []string{}
|
|
|
|
//notify endpoints that they may have new service posture queries
|
|
for identityId := range newIdentityServiceUpdates {
|
|
if _, ok := completedIdentityServiceUpdates[identityId]; !ok {
|
|
completedIdentityServiceUpdates[identityId] = struct{}{}
|
|
pc.env.HandleServiceUpdatedEventForIdentityId(identityId)
|
|
}
|
|
}
|
|
|
|
newIdentityServiceUpdates = map[string]struct{}{}
|
|
}
|
|
}
|
|
|
|
func (pc *PostureCache) Add(identityId string, postureResponses []*PostureResponse) {
|
|
pc.Upsert(identityId, true, func(exist bool, valueInMap *PostureData, newValue *PostureData) *PostureData {
|
|
var postureData *PostureData
|
|
|
|
if exist {
|
|
postureData = valueInMap
|
|
} else {
|
|
postureData = newValue
|
|
}
|
|
|
|
for _, postureResponse := range postureResponses {
|
|
postureResponse.Apply(postureData)
|
|
}
|
|
|
|
return postureData
|
|
})
|
|
|
|
pc.Emit(EventIdentityPostureDataAltered, identityId)
|
|
}
|
|
|
|
// Upsert is a convenience function to alter the existing PostureData for an identity. If
|
|
// emitDataAltered is true, posture data listeners will be alerted: this will trigger
|
|
// service update notifications and posture check evaluation.
|
|
func (pc *PostureCache) Upsert(identityId string, emitDataAltered bool, cb func(exist bool, valueInMap *PostureData, newValue *PostureData) *PostureData) {
|
|
pc.identityToPostureData.Upsert(identityId, newPostureData(), cb)
|
|
|
|
if emitDataAltered {
|
|
pc.Emit(EventIdentityPostureDataAltered, identityId)
|
|
}
|
|
}
|
|
|
|
const MaxPostureFailures = 100
|
|
|
|
func (pc *PostureCache) AddSessionRequestFailure(identityId string, failure *PostureSessionRequestFailure) {
|
|
pc.identityToPostureData.Upsert(identityId, newPostureData(), func(exist bool, valueInMap *PostureData, newValue *PostureData) *PostureData {
|
|
var postureData *PostureData
|
|
|
|
if exist {
|
|
postureData = valueInMap
|
|
} else {
|
|
postureData = newValue
|
|
}
|
|
|
|
postureData.SessionRequestFailures = append(postureData.SessionRequestFailures, failure)
|
|
|
|
if len(postureData.SessionRequestFailures) > MaxPostureFailures {
|
|
postureData.SessionRequestFailures = postureData.SessionRequestFailures[1:]
|
|
}
|
|
|
|
return postureData
|
|
})
|
|
}
|
|
|
|
func (pc *PostureCache) Evaluate(identityId, apiSessionId string, postureChecks []*PostureCheck) (bool, []*PostureCheckFailure) {
|
|
if postureData, found := pc.identityToPostureData.Get(identityId); found {
|
|
return postureData.Evaluate(apiSessionId, postureChecks)
|
|
}
|
|
|
|
//mock failures with nil provided data, no posture data found
|
|
var failures []*PostureCheckFailure
|
|
for _, check := range postureChecks {
|
|
failures = append(failures, &PostureCheckFailure{
|
|
PostureCheckId: check.Id,
|
|
PostureCheckName: check.Name,
|
|
PostureCheckType: check.TypeId,
|
|
PostureCheckFailureValues: check.SubType.FailureValues("", newPostureData()),
|
|
})
|
|
}
|
|
|
|
return false, failures
|
|
}
|
|
|
|
// PostureData returns a copy of the current posture data for an identity.
|
|
// Suitable for read only rendering. To alter/update posture data see Upsert.
|
|
func (pc *PostureCache) PostureData(identityId string) *PostureData {
|
|
var result *PostureData = nil
|
|
|
|
pc.Upsert(identityId, false, func(exist bool, valueInMap *PostureData, newValue *PostureData) *PostureData {
|
|
var pd *PostureData
|
|
if exist {
|
|
pd = valueInMap
|
|
} else {
|
|
pd = newValue
|
|
}
|
|
|
|
result = pd.Copy()
|
|
return pd
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
func (pc *PostureCache) WithPostureData(identityId string, f func(data *PostureData)) {
|
|
pc.Upsert(identityId, false, func(exist bool, valueInMap *PostureData, newValue *PostureData) *PostureData {
|
|
var pd *PostureData
|
|
if exist {
|
|
pd = valueInMap
|
|
} else {
|
|
pd = newValue
|
|
}
|
|
f(pd)
|
|
return pd
|
|
})
|
|
}
|
|
|
|
func (pc *PostureCache) ApiSessionCreated(apiSession *db.ApiSession) {
|
|
pc.apiSessionIdToIdentityId.Set(apiSession.Id, apiSession.IdentityId)
|
|
}
|
|
|
|
func (pc *PostureCache) ApiSessionDeleted(apiSession *db.ApiSession) {
|
|
pc.identityToPostureData.Upsert(apiSession.IdentityId, newPostureData(), func(exist bool, valueInMap *PostureData, newValue *PostureData) *PostureData {
|
|
if exist {
|
|
if valueInMap != nil && valueInMap.ApiSessions != nil {
|
|
delete(valueInMap.ApiSessions, apiSession.Id)
|
|
}
|
|
|
|
return valueInMap
|
|
}
|
|
|
|
return newValue
|
|
})
|
|
|
|
pc.apiSessionIdToIdentityId.Remove(apiSession.Id)
|
|
}
|
|
|
|
func (pc *PostureCache) IdentityDeleted(identity *db.Identity) {
|
|
pc.identityToPostureData.Remove(identity.Id)
|
|
}
|
|
|
|
// PostureCheckChanged notifies all associated identities that posture configuration has changed
|
|
// and that endpoints may need to reevaluate posture queries.
|
|
func (pc *PostureCache) PostureCheckChanged(entity boltz.Entity) {
|
|
servicePolicyLinks := pc.env.GetStores().PostureCheck.GetLinkCollection(db.EntityTypeServicePolicies)
|
|
|
|
if servicePolicyLinks == nil {
|
|
pfxlog.Logger().Error("posture checks had no links to service policies")
|
|
return
|
|
}
|
|
|
|
identitiesToNotify := map[string]struct{}{}
|
|
|
|
_ = pc.env.GetDb().View(func(tx *bbolt.Tx) error {
|
|
servicePolicyCursor := servicePolicyLinks.IterateLinks(tx, []byte(entity.GetId()))
|
|
|
|
for servicePolicyCursor.IsValid() {
|
|
identityLink := pc.env.GetStores().ServicePolicy.GetLinkCollection(db.EntityTypeIdentities)
|
|
|
|
if identityLink == nil {
|
|
pfxlog.Logger().Error("service policies had no link to identities")
|
|
return nil
|
|
}
|
|
|
|
identityCursor := identityLink.IterateLinks(tx, servicePolicyCursor.Current())
|
|
|
|
for identityCursor.IsValid() {
|
|
identitiesToNotify[string(identityCursor.Current())] = struct{}{}
|
|
}
|
|
|
|
servicePolicyCursor.Next()
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
for identityId := range identitiesToNotify {
|
|
pc.env.HandleServiceUpdatedEventForIdentityId(identityId)
|
|
}
|
|
}
|
|
|
|
type PostureSessionData struct {
|
|
MfaTimeout int64
|
|
}
|
|
|
|
type ApiSessionPostureData struct {
|
|
Mfa *PostureResponseMfa `json:"mfa"`
|
|
EndpointState *PostureResponseEndpointState `json:"endpointState"`
|
|
SdkInfo *SdkInfo
|
|
}
|
|
|
|
func (self *ApiSessionPostureData) GetPassedMfaAt() *time.Time {
|
|
if self == nil || self.Mfa == nil {
|
|
return nil
|
|
}
|
|
return self.Mfa.PassedMfaAt
|
|
}
|
|
|
|
type PostureCheckFailureSubType interface {
|
|
Value() interface{}
|
|
Expected() interface{}
|
|
}
|
|
|
|
type PostureCheckFailure struct {
|
|
PostureCheckId string `json:"postureCheckId"`
|
|
PostureCheckName string `json:"postureCheckName"`
|
|
PostureCheckType string `json:"postureCheckType"`
|
|
PostureCheckFailureValues
|
|
}
|
|
|
|
func (self PostureCheckFailure) ToClientErrorData() interface{} {
|
|
return map[string]interface{}{
|
|
"id": self.PostureCheckId,
|
|
"typeId": self.PostureCheckType,
|
|
"isPassing": false,
|
|
}
|
|
}
|
|
|
|
type PosturePolicyFailure struct {
|
|
PolicyId string
|
|
PolicyName string
|
|
Checks []*PostureCheckFailure
|
|
}
|
|
|
|
type PostureSessionRequestFailure struct {
|
|
When time.Time
|
|
ServiceId string
|
|
ServiceName string
|
|
SessionType string
|
|
PolicyFailures []*PosturePolicyFailure
|
|
ApiSessionId string
|
|
}
|
|
|
|
type PostureData struct {
|
|
Mac PostureResponseMac
|
|
Domain PostureResponseDomain
|
|
Os PostureResponseOs
|
|
Processes []*PostureResponseProcess
|
|
ProcessPathMap map[string]*PostureResponseProcess
|
|
ApiSessions map[string]*ApiSessionPostureData
|
|
SessionRequestFailures []*PostureSessionRequestFailure
|
|
}
|
|
|
|
func (pd *PostureData) Evaluate(apiSessionId string, checks []*PostureCheck) (bool, []*PostureCheckFailure) {
|
|
|
|
var failures []*PostureCheckFailure
|
|
for _, check := range checks {
|
|
if isValid, failure := check.Evaluate(apiSessionId, pd); !isValid {
|
|
failures = append(failures, failure)
|
|
}
|
|
}
|
|
|
|
return len(failures) == 0, failures
|
|
}
|
|
|
|
func (pd *PostureData) Copy() *PostureData {
|
|
dest := &PostureData{}
|
|
_ = copier.Copy(dest, pd)
|
|
return dest
|
|
}
|
|
|
|
func newPostureData() *PostureData {
|
|
ret := &PostureData{
|
|
Mac: PostureResponseMac{
|
|
PostureResponse: &PostureResponse{},
|
|
Addresses: []string{},
|
|
},
|
|
Domain: PostureResponseDomain{
|
|
PostureResponse: &PostureResponse{},
|
|
Name: "",
|
|
},
|
|
Os: PostureResponseOs{
|
|
PostureResponse: &PostureResponse{},
|
|
Type: "",
|
|
Version: "",
|
|
Build: "",
|
|
},
|
|
Processes: []*PostureResponseProcess{},
|
|
ProcessPathMap: map[string]*PostureResponseProcess{},
|
|
ApiSessions: map[string]*ApiSessionPostureData{},
|
|
SessionRequestFailures: []*PostureSessionRequestFailure{},
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
type PostureResponse struct {
|
|
PostureCheckId string
|
|
TypeId string
|
|
TimedOut bool
|
|
LastUpdatedAt time.Time
|
|
SubType PostureResponseSubType
|
|
}
|
|
|
|
func (pr *PostureResponse) Apply(postureData *PostureData) {
|
|
pr.SubType.Apply(postureData)
|
|
}
|
|
|
|
type PostureResponseSubType interface {
|
|
Apply(postureData *PostureData)
|
|
}
|
|
|
|
var macClean = regexp.MustCompile(`[^a-f\d]+`)
|
|
|
|
func CleanHexString(hexString string) string {
|
|
return macClean.ReplaceAllString(strings.ToLower(hexString), "")
|
|
}
|