mirror of
https://github.com/openziti/ziti.git
synced 2026-09-11 13:29:03 +00:00
ff619272ba
The router's CheckConnections reaper enforced only JWT expiry, so a revoked OIDC api-session, or a disabled/deleted identity, kept its live circuits and hosted terminators until the access token expired. The router now enforces the RouterDataModel revocations directly, tightening access-loss propagation to the reaper interval. - adds a Type field to DataState_Revocation and the raft Revocation command proto, mirroring rest_model.RevocationTypeEnum (API_SESSION/IDENTITY/JTI) so the management API, OIDC producers, sync, and router enforcement share one vocabulary; the common.RevocationType* constants are compile-time bound to the enum to prevent drift - adds an IssuedBefore cutoff so an identity revocation invalidates only sessions issued before it; a session re-authenticated after the cutoff survives the still-lingering revocation. Persists IssuedBefore on the db and model Revocation and carries it (plus the Type) through the single and batched raft marshalling - adds RouterDataModel.IsApiSessionRevoked and IsIdentityRevoked and enforces both in CheckConnections, closing a revoked session's connections - revokes a deleted or disabled identity's live OIDC sessions via an IdentityRevocationConstraint in the db package, run as a store pre-commit constraint so the revocation is written in the same transaction as the identity change and cannot be skipped (self-contained OIDC JWTs aren't otherwise reachable). NewIdentityManager installs it with the revocation type and lifetime - has the OIDC end-session (TerminateSessionFromRequest) revoke the specific api-session named by the z_asid claim, with an identity-scoped fallback; sets IssuedBefore on the identity fallback and the management revocation API; adds RevocationManager.CreateOrReplace, routed through by both the OIDC paths and the management revocation API, so a repeat logout/termination/revocation refreshes the cutoff rather than colliding on the reused id. Expiry derives from the longest configured token duration via a shared common.MaxTokenDuration helper - adds tests/revocation_enforcement_oidc_test.go covering api-session revocation (and a fresh session staying unaffected), identity disable and delete, and the identity cutoff (a post-cutoff session surviving the lingering revocation)
1398 lines
42 KiB
Go
1398 lines
42 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 (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/foundation/v2/errorz"
|
|
"github.com/openziti/metrics"
|
|
"github.com/openziti/sdk-golang/ziti"
|
|
"github.com/openziti/ziti/v2/common"
|
|
"github.com/openziti/ziti/v2/common/ctrlchan"
|
|
"github.com/openziti/ziti/v2/common/eid"
|
|
"github.com/openziti/ziti/v2/common/inspect"
|
|
"github.com/openziti/ziti/v2/common/pb/cmd_pb"
|
|
"github.com/openziti/ziti/v2/common/pb/edge_cmd_pb"
|
|
"github.com/openziti/ziti/v2/common/pb/edge_ctrl_pb"
|
|
"github.com/openziti/ziti/v2/controller/change"
|
|
"github.com/openziti/ziti/v2/controller/command"
|
|
"github.com/openziti/ziti/v2/controller/config"
|
|
"github.com/openziti/ziti/v2/controller/db"
|
|
"github.com/openziti/ziti/v2/controller/event"
|
|
"github.com/openziti/ziti/v2/controller/fields"
|
|
"github.com/openziti/ziti/v2/controller/models"
|
|
"github.com/openziti/ziti/v2/controller/permissions"
|
|
"github.com/openziti/ziti/v2/controller/storage/boltz"
|
|
cmap "github.com/orcaman/concurrent-map/v2"
|
|
"go.etcd.io/bbolt"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
const (
|
|
IdentityActiveIntervalSeconds = 60
|
|
|
|
minDefaultAdminPasswordLength = 5
|
|
maxDefaultAdminPasswordLength = 100
|
|
minDefaultAdminUsernameLength = 4
|
|
maxDefaultAdminUsernameLength = 100
|
|
minDefaultAdminNameLength = 4
|
|
maxDefaultAdminNameLength = 100
|
|
)
|
|
|
|
type IdentityManager struct {
|
|
baseEntityManager[*Identity, *db.Identity]
|
|
updateSdkInfoTimer metrics.Timer
|
|
identityStatusMap *identityStatusMap
|
|
connections *ConnectionTracker
|
|
statusSource config.IdentityStatusSource
|
|
}
|
|
|
|
func NewIdentityManager(env Env) *IdentityManager {
|
|
manager := &IdentityManager{
|
|
baseEntityManager: newBaseEntityManager[*Identity, *db.Identity](env, env.GetStores().Identity),
|
|
updateSdkInfoTimer: env.GetMetricsRegistry().Timer("identity.update-sdk-info"),
|
|
identityStatusMap: newIdentityStatusMap(IdentityActiveIntervalSeconds * time.Second),
|
|
connections: newConnectionTracker(env),
|
|
statusSource: env.GetConfig().Edge.IdentityStatusConfig.Source,
|
|
}
|
|
manager.impl = manager
|
|
|
|
RegisterManagerDecoder[*Identity](env, manager)
|
|
RegisterCommand(env, &CreateIdentityWithEnrollmentsCmd{}, &edge_cmd_pb.CreateIdentityWithEnrollmentsCmd{})
|
|
RegisterCommand(env, &CreateIdentityWithAuthenticatorsCmd{}, &edge_cmd_pb.CreateIdentityWithAuthenticatorsCmd{})
|
|
RegisterCommand(env, &UpdateServiceConfigsCmd{}, &edge_cmd_pb.UpdateServiceConfigsCmd{})
|
|
|
|
// Revoke a deleted or disabled identity's live OIDC sessions atomically with
|
|
// the identity change itself, so the revocation cannot be skipped. The db-level
|
|
// constraint is config-agnostic, so the revocation type and lifetime are
|
|
// supplied here.
|
|
env.GetStores().Identity.AddEntityConstraint(db.NewIdentityRevocationConstraint(
|
|
env.GetStores().Revocation,
|
|
common.RevocationTypeIdentity,
|
|
func() time.Duration { return env.GetConfig().Edge.Oidc.RefreshTokenDuration },
|
|
))
|
|
|
|
return manager
|
|
}
|
|
|
|
func (self *IdentityManager) NewModelEntity() *Identity {
|
|
return &Identity{}
|
|
}
|
|
|
|
func (self *IdentityManager) validateIdentityPermissions(entity *Identity) error {
|
|
for _, permission := range entity.Permissions {
|
|
if _, valid := permissions.AllPermissions[permission]; !valid {
|
|
reason := fmt.Sprintf("invalid permissions '%s'", permission)
|
|
return errorz.NewFieldApiError(errorz.NewFieldError(reason, "permissions", permission))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *IdentityManager) Create(entity *Identity, ctx *change.Context) error {
|
|
if err := self.validateIdentityPermissions(entity); err != nil {
|
|
return err
|
|
}
|
|
return DispatchCreate[*Identity](self, entity, ctx)
|
|
}
|
|
|
|
func (self *IdentityManager) ApplyCreate(cmd *command.CreateEntityCommand[*Identity], ctx boltz.MutateContext) error {
|
|
_, err := self.createEntity(cmd.Entity, ctx)
|
|
return err
|
|
}
|
|
|
|
func (self *IdentityManager) CreateWithEnrollments(identityModel *Identity, enrollmentsModels []*Enrollment, ctx *change.Context) error {
|
|
if identityModel.Id == "" {
|
|
identityModel.Id = eid.New()
|
|
}
|
|
|
|
for _, enrollment := range enrollmentsModels {
|
|
if enrollment.Id == "" {
|
|
enrollment.Id = eid.New()
|
|
}
|
|
enrollment.IdentityId = &identityModel.Id
|
|
}
|
|
|
|
if err := self.validateIdentityPermissions(identityModel); err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := &CreateIdentityWithEnrollmentsCmd{
|
|
manager: self,
|
|
identity: identityModel,
|
|
enrollments: enrollmentsModels,
|
|
ctx: ctx,
|
|
}
|
|
|
|
return self.Dispatch(cmd)
|
|
}
|
|
|
|
func (self *IdentityManager) ApplyCreateWithEnrollments(cmd *CreateIdentityWithEnrollmentsCmd, ctx boltz.MutateContext) error {
|
|
identityModel := cmd.identity
|
|
enrollmentsModels := cmd.enrollments
|
|
|
|
return self.GetDb().Update(ctx, func(ctx boltz.MutateContext) error {
|
|
boltEntity, err := identityModel.toBoltEntityForCreate(ctx.Tx(), self.env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = self.GetStore().Create(ctx, boltEntity); err != nil {
|
|
pfxlog.Logger().WithError(err).Errorf("could not create %v in bolt storage", self.GetStore().GetSingularEntityType())
|
|
return err
|
|
}
|
|
|
|
for _, enrollment := range enrollmentsModels {
|
|
enrollment.IdentityId = &identityModel.Id
|
|
|
|
if err = enrollment.FillJwtInfoForIdentity(self.env, identityModel.Id); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = self.env.GetManagers().Enrollment.createEntityInTx(ctx, enrollment); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (self *IdentityManager) CreateWithAuthenticators(identity *Identity, authenticators []*Authenticator, ctx *change.Context) (string, []string, error) {
|
|
if identity.Id == "" {
|
|
identity.Id = eid.New()
|
|
}
|
|
|
|
var authenticatorIds []string
|
|
for _, authenticator := range authenticators {
|
|
if authenticator.Id == "" {
|
|
authenticator.Id = eid.New()
|
|
}
|
|
authenticator.IdentityId = identity.Id
|
|
authenticatorIds = append(authenticatorIds, authenticator.Id)
|
|
}
|
|
|
|
identityType, err := self.env.GetManagers().IdentityType.ReadByIdOrName(identity.IdentityTypeId)
|
|
|
|
if err != nil && !boltz.IsErrNotFoundErr(err) {
|
|
return "", nil, err
|
|
}
|
|
|
|
if identityType == nil {
|
|
apiErr := errorz.NewNotFound()
|
|
apiErr.Cause = errorz.NewFieldError("typeId not found", "typeId", identity.IdentityTypeId)
|
|
apiErr.AppendCause = true
|
|
return "", nil, apiErr
|
|
}
|
|
|
|
cmd := &CreateIdentityWithAuthenticatorsCmd{
|
|
manager: self,
|
|
identity: identity,
|
|
authenticators: authenticators,
|
|
ctx: ctx,
|
|
}
|
|
|
|
err = self.Dispatch(cmd)
|
|
|
|
if err != nil {
|
|
return "", nil, nil
|
|
}
|
|
|
|
return identity.Id, authenticatorIds, nil
|
|
}
|
|
|
|
func (self *IdentityManager) ApplyCreateWithAuthenticators(cmd *CreateIdentityWithAuthenticatorsCmd, ctx boltz.MutateContext) error {
|
|
return self.env.GetDb().Update(ctx, func(ctx boltz.MutateContext) error {
|
|
boltIdentity, err := cmd.identity.toBoltEntityForCreate(ctx.Tx(), self.env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = self.env.GetStores().Identity.Create(ctx, boltIdentity); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, authenticator := range cmd.authenticators {
|
|
boltAuthenticator, err := authenticator.toBoltEntityForCreate(ctx.Tx(), self.env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = self.env.GetStores().Authenticator.Create(ctx, boltAuthenticator); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (self *IdentityManager) Update(entity *Identity, checker fields.UpdatedFields, ctx *change.Context) error {
|
|
if checker == nil || checker.IsUpdated(db.FieldIdentityPermissions) {
|
|
if err := self.validateIdentityPermissions(entity); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return DispatchUpdate[*Identity](self, entity, checker, ctx)
|
|
}
|
|
|
|
func (self *IdentityManager) ApplyUpdate(cmd *command.UpdateEntityCommand[*Identity], ctx boltz.MutateContext) error {
|
|
var checker boltz.FieldChecker
|
|
|
|
if cmd.UpdatedFields == nil {
|
|
checker = &AndFieldChecker{
|
|
first: self,
|
|
second: NotFieldChecker{
|
|
db.FieldIdentityServiceConfigs: struct{}{},
|
|
},
|
|
}
|
|
} else {
|
|
checker = &AndFieldChecker{first: self, second: cmd.UpdatedFields}
|
|
}
|
|
return self.updateEntity(cmd.Entity, checker, ctx)
|
|
}
|
|
|
|
func (self *IdentityManager) IsUpdated(field string) bool {
|
|
return field != db.FieldIdentityAuthenticators && field != db.FieldIdentityEnrollments && field != db.FieldIdentityIsDefaultAdmin
|
|
}
|
|
|
|
func (self *IdentityManager) ReadByName(name string) (*Identity, error) {
|
|
entity := &Identity{}
|
|
nameIndex := self.env.GetStores().Identity.GetNameIndex()
|
|
if err := self.readEntityWithIndex("name", []byte(name), nameIndex, entity); err != nil {
|
|
return nil, err
|
|
}
|
|
return entity, nil
|
|
}
|
|
|
|
func (self *IdentityManager) ReadDefaultAdmin() (*Identity, error) {
|
|
return self.ReadOneByQuery("isDefaultAdmin = true")
|
|
}
|
|
|
|
func (self *IdentityManager) ReadOneByQuery(query string) (*Identity, error) {
|
|
result, err := self.readEntityByQuery(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil {
|
|
return nil, nil
|
|
}
|
|
return result.(*Identity), nil
|
|
}
|
|
|
|
func (self *IdentityManager) InitializeDefaultAdmin(username, password, name string) error {
|
|
if len(username) < minDefaultAdminUsernameLength {
|
|
return errorz.NewFieldError(fmt.Sprintf("username must be at least %v characters", minDefaultAdminUsernameLength), "username", username)
|
|
}
|
|
if len(password) < minDefaultAdminPasswordLength {
|
|
return errorz.NewFieldError(fmt.Sprintf("password must be at least %v characters", minDefaultAdminPasswordLength), "password", "******")
|
|
}
|
|
if len(name) < minDefaultAdminNameLength {
|
|
return errorz.NewFieldError(fmt.Sprintf("name must be at least %v characters", minDefaultAdminNameLength), "name", name)
|
|
}
|
|
|
|
if len(username) > maxDefaultAdminUsernameLength {
|
|
return errorz.NewFieldError(fmt.Sprintf("username must be at most %v characters", maxDefaultAdminUsernameLength), "username", username)
|
|
}
|
|
if len(password) > maxDefaultAdminPasswordLength {
|
|
return errorz.NewFieldError(fmt.Sprintf("password must be at most %v characters", maxDefaultAdminPasswordLength), "password", "******")
|
|
}
|
|
if len(name) > maxDefaultAdminNameLength {
|
|
return errorz.NewFieldError(fmt.Sprintf("name must be at most %v characters", maxDefaultAdminNameLength), "name", name)
|
|
}
|
|
|
|
identityType, err := self.env.GetManagers().IdentityType.ReadByName(db.DefaultIdentityType)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
identity, err := self.ReadDefaultAdmin()
|
|
|
|
if err != nil && !boltz.IsErrNotFoundErr(err) {
|
|
return err
|
|
}
|
|
|
|
if identity != nil {
|
|
return errors.New("already initialized: Ziti Edge default admin already defined")
|
|
}
|
|
|
|
if err = self.env.GetManagers().Dispatcher.Bootstrap(); err != nil {
|
|
return fmt.Errorf("unable to bootstrap command dispatcher (%w)", err)
|
|
}
|
|
|
|
identityId := eid.New()
|
|
authenticatorId := eid.New()
|
|
|
|
defaultAdmin := &Identity{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: identityId,
|
|
},
|
|
Name: name,
|
|
IdentityTypeId: identityType.Id,
|
|
IsDefaultAdmin: true,
|
|
IsAdmin: true,
|
|
}
|
|
|
|
authenticator := &Authenticator{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: authenticatorId,
|
|
},
|
|
Method: db.MethodAuthenticatorUpdb,
|
|
IdentityId: identityId,
|
|
SubType: &AuthenticatorUpdb{
|
|
Username: username,
|
|
Password: password,
|
|
},
|
|
}
|
|
|
|
ctx := change.New().SetSourceType("cli.init").SetChangeAuthorType(change.AuthorTypeController)
|
|
if err = self.Create(defaultAdmin, ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = self.env.GetManagers().Authenticator.Create(authenticator, ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *IdentityManager) CollectAuthenticators(id string, collector func(entity *Authenticator) error) error {
|
|
return self.GetDb().View(func(tx *bbolt.Tx) error {
|
|
_, err := self.readInTx(tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
authenticatorIds := self.GetStore().GetRelatedEntitiesIdList(tx, id, db.FieldIdentityAuthenticators)
|
|
for _, authenticatorId := range authenticatorIds {
|
|
authenticator := &Authenticator{}
|
|
err := self.env.GetManagers().Authenticator.readEntityInTx(tx, authenticatorId, authenticator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = collector(authenticator); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (self *IdentityManager) visitAuthenticators(tx *bbolt.Tx, id string, visitor func(entity *Authenticator) bool) error {
|
|
_, err := self.readInTx(tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
authenticatorIds := self.GetStore().GetRelatedEntitiesIdList(tx, id, db.FieldIdentityAuthenticators)
|
|
for _, authenticatorId := range authenticatorIds {
|
|
authenticator := &Authenticator{}
|
|
if err := self.env.GetManagers().Authenticator.readEntityInTx(tx, authenticatorId, authenticator); err != nil {
|
|
return err
|
|
}
|
|
if visitor(authenticator) {
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
func (self *IdentityManager) CollectEnrollments(id string, collector func(entity *Enrollment) error) error {
|
|
return self.GetDb().View(func(tx *bbolt.Tx) error {
|
|
return self.collectEnrollmentsInTx(tx, id, collector)
|
|
})
|
|
}
|
|
|
|
func (self *IdentityManager) collectEnrollmentsInTx(tx *bbolt.Tx, id string, collector func(entity *Enrollment) error) error {
|
|
_, err := self.readInTx(tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
associationIds := self.GetStore().GetRelatedEntitiesIdList(tx, id, db.FieldIdentityEnrollments)
|
|
for _, enrollmentId := range associationIds {
|
|
enrollment, err := self.env.GetManagers().Enrollment.readInTx(tx, enrollmentId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = collector(enrollment)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *IdentityManager) AssignServiceConfigs(id string, serviceConfigs []ServiceConfig, ctx *change.Context) error {
|
|
cmd := &UpdateServiceConfigsCmd{
|
|
manager: self,
|
|
identityId: id,
|
|
add: true,
|
|
serviceConfigs: serviceConfigs,
|
|
ctx: ctx,
|
|
}
|
|
return self.Dispatch(cmd)
|
|
}
|
|
|
|
func (self *IdentityManager) RemoveServiceConfigs(id string, serviceConfigs []ServiceConfig, ctx *change.Context) error {
|
|
cmd := &UpdateServiceConfigsCmd{
|
|
manager: self,
|
|
identityId: id,
|
|
add: false,
|
|
serviceConfigs: serviceConfigs,
|
|
ctx: ctx,
|
|
}
|
|
return self.Dispatch(cmd)
|
|
}
|
|
|
|
func (self *IdentityManager) ApplyUpdateServiceConfigs(cmd *UpdateServiceConfigsCmd, ctx boltz.MutateContext) error {
|
|
identityStore := self.env.GetStores().Identity
|
|
return self.GetDb().Update(ctx, func(ctx boltz.MutateContext) error {
|
|
identity, err := identityStore.LoadById(ctx.Tx(), cmd.identityId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, serviceConfig := range cmd.serviceConfigs {
|
|
cfg, err := self.env.GetStores().Config.LoadById(ctx.Tx(), serviceConfig.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cmd.add {
|
|
configType, _ := self.env.GetStores().ConfigType.LoadById(ctx.Tx(), cfg.TypeId)
|
|
if configType != nil && configType.Target != db.ConfigTypeTargetService {
|
|
return errorz.NewFieldError(fmt.Sprintf("config %v has config type %v which does not target services",
|
|
cfg.Name, configType.Name), "serviceConfigs", cmd.serviceConfigs)
|
|
}
|
|
if identity.ServiceConfigs == nil {
|
|
identity.ServiceConfigs = map[string]map[string]string{}
|
|
}
|
|
serviceMap, ok := identity.ServiceConfigs[serviceConfig.Service]
|
|
if !ok {
|
|
serviceMap = map[string]string{}
|
|
identity.ServiceConfigs[serviceConfig.Service] = serviceMap
|
|
}
|
|
serviceMap[cfg.TypeId] = cfg.Id
|
|
} else if identity.ServiceConfigs != nil {
|
|
if serviceMap, ok := identity.ServiceConfigs[serviceConfig.Service]; ok {
|
|
delete(serviceMap, cfg.TypeId)
|
|
}
|
|
}
|
|
}
|
|
|
|
return identityStore.Update(ctx, identity, boltz.MapFieldChecker{
|
|
db.FieldIdentityServiceConfigs: struct{}{},
|
|
})
|
|
})
|
|
}
|
|
|
|
func (self *IdentityManager) QueryRoleAttributes(queryString string) ([]string, *models.QueryMetaData, error) {
|
|
index := self.env.GetStores().Identity.GetRoleAttributesIndex()
|
|
return self.queryRoleAttributes(index, queryString)
|
|
}
|
|
|
|
// PatchInfo updates the identity's envInfo and sdkInfo fields based on the supplied fields.
|
|
// The fields to update are determined by the checker. If checker is nil, all fields are updated.
|
|
func (self *IdentityManager) PatchInfo(identity *Identity, checker fields.UpdatedFields, changeCtx *change.Context) {
|
|
start := time.Now()
|
|
|
|
if checker == nil {
|
|
checker = fields.UpdatedFieldsMap{
|
|
db.FieldIdentityEnvInfoArch: struct{}{},
|
|
db.FieldIdentityEnvInfoOs: struct{}{},
|
|
db.FieldIdentityEnvInfoOsRelease: struct{}{},
|
|
db.FieldIdentityEnvInfoOsVersion: struct{}{},
|
|
db.FieldIdentityEnvInfoDomain: struct{}{},
|
|
db.FieldIdentityEnvInfoHostname: struct{}{},
|
|
db.FieldIdentitySdkInfoBranch: struct{}{},
|
|
db.FieldIdentitySdkInfoRevision: struct{}{},
|
|
db.FieldIdentitySdkInfoType: struct{}{},
|
|
db.FieldIdentitySdkInfoVersion: struct{}{},
|
|
db.FieldIdentitySdkInfoAppId: struct{}{},
|
|
db.FieldIdentitySdkInfoAppVersion: struct{}{},
|
|
}
|
|
}
|
|
|
|
err := self.Update(identity, checker, changeCtx)
|
|
|
|
if err != nil {
|
|
pfxlog.Logger().Warnf("unable to update identity %s sdk/env info: %v", identity.Id, err)
|
|
}
|
|
|
|
self.updateSdkInfoTimer.UpdateSince(start)
|
|
}
|
|
|
|
func (self *IdentityManager) GetConnectionTracker() *ConnectionTracker {
|
|
return self.connections
|
|
}
|
|
|
|
// SetHasErConnection will register an identity as having an ER connection. The registration has a TTL depending on
|
|
// how the status map was configured.
|
|
func (self *IdentityManager) SetHasErConnection(identityId string) {
|
|
self.identityStatusMap.SetHasEdgeRouterConnection(identityId)
|
|
}
|
|
|
|
// HasErConnection will return true if the supplied identity id has a current an active ER connection registered.
|
|
func (self *IdentityManager) HasErConnection(id string) bool {
|
|
if self.statusSource == config.IdentityStatusSourceConnectEvents {
|
|
return self.connections.GetIdentityOnlineState(id) == IdentityStateOnline
|
|
}
|
|
if self.statusSource == config.IdentityStatusSourceHeartbeats {
|
|
return self.identityStatusMap.HasEdgeRouterConnection(id)
|
|
}
|
|
return self.connections.GetIdentityOnlineState(id) == IdentityStateOnline || self.identityStatusMap.HasEdgeRouterConnection(id)
|
|
}
|
|
|
|
func (self *IdentityManager) VisitIdentityAuthenticatorFingerprints(tx *bbolt.Tx, identityId string, visitor func(string) bool) (bool, error) {
|
|
stopVisit := false
|
|
err := self.visitAuthenticators(tx, identityId, func(authenticator *Authenticator) bool {
|
|
for _, authPrint := range authenticator.Fingerprints() {
|
|
if visitor(authPrint) {
|
|
stopVisit = true
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
return stopVisit, err
|
|
}
|
|
|
|
func (self *IdentityManager) ReadByExternalId(externalId string) (*Identity, error) {
|
|
modelEntity := &Identity{}
|
|
index := self.env.GetStores().Identity.GetExternalIdIndex()
|
|
if err := self.readEntityWithIndex("externalId", []byte(externalId), index, modelEntity); err != nil {
|
|
if boltz.IsErrNotFoundErr(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return modelEntity, nil
|
|
}
|
|
|
|
func (self *IdentityManager) Disable(identityId string, duration time.Duration, ctx *change.Context) error {
|
|
if duration < 0 {
|
|
duration = 0
|
|
}
|
|
|
|
fieldMap := fields.UpdatedFieldsMap{
|
|
db.FieldIdentityDisabledAt: struct{}{},
|
|
db.FieldIdentityDisabledUntil: struct{}{},
|
|
}
|
|
|
|
lockedAt := time.Now()
|
|
var lockedUntil *time.Time
|
|
|
|
if duration != 0 {
|
|
until := lockedAt.Add(duration)
|
|
lockedUntil = &until
|
|
}
|
|
|
|
err := self.Update(&Identity{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: identityId,
|
|
},
|
|
DisabledAt: &lockedAt,
|
|
DisabledUntil: lockedUntil,
|
|
}, fieldMap, ctx)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Disabling sets DisabledAt, which the IdentityRevocationConstraint observes
|
|
// to revoke the identity's live OIDC sessions in the same transaction.
|
|
return self.GetEnv().GetManagers().ApiSession.DeleteByIdentityId(identityId, ctx)
|
|
}
|
|
|
|
func (self *IdentityManager) Enable(identityId string, ctx *change.Context) error {
|
|
fieldMap := fields.UpdatedFieldsMap{
|
|
db.FieldIdentityDisabledAt: struct{}{},
|
|
db.FieldIdentityDisabledUntil: struct{}{},
|
|
}
|
|
|
|
return self.Update(&Identity{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: identityId,
|
|
},
|
|
DisabledAt: nil,
|
|
DisabledUntil: nil,
|
|
}, fieldMap, ctx)
|
|
}
|
|
|
|
func (self *IdentityManager) GetIdentityStatusMapCopy() map[string]map[string]ctrlchan.CtrlChannel {
|
|
result := map[string]map[string]ctrlchan.CtrlChannel{}
|
|
for entry := range self.connections.connections.IterBuffered() {
|
|
routerMap := map[string]ctrlchan.CtrlChannel{}
|
|
entry.Val.Lock()
|
|
for routerId, ch := range entry.Val.routers {
|
|
routerMap[routerId] = ch
|
|
}
|
|
entry.Val.Unlock()
|
|
result[entry.Key] = routerMap
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (self *IdentityManager) IdentityToProtobuf(entity *Identity) (*edge_cmd_pb.Identity, error) {
|
|
tags, err := edge_cmd_pb.EncodeTags(entity.Tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var envInfo *edge_cmd_pb.Identity_EnvInfo
|
|
if entity.EnvInfo != nil {
|
|
envInfo = &edge_cmd_pb.Identity_EnvInfo{
|
|
Arch: entity.EnvInfo.Arch,
|
|
Os: entity.EnvInfo.Os,
|
|
OsRelease: entity.EnvInfo.OsRelease,
|
|
OsVersion: entity.EnvInfo.OsVersion,
|
|
Domain: entity.EnvInfo.Domain,
|
|
Hostname: entity.EnvInfo.Hostname,
|
|
}
|
|
}
|
|
|
|
var sdkInfo *edge_cmd_pb.Identity_SdkInfo
|
|
if entity.SdkInfo != nil {
|
|
sdkInfo = &edge_cmd_pb.Identity_SdkInfo{
|
|
AppId: entity.SdkInfo.AppId,
|
|
AppVersion: entity.SdkInfo.AppVersion,
|
|
Branch: entity.SdkInfo.Branch,
|
|
Revision: entity.SdkInfo.Revision,
|
|
Type: entity.SdkInfo.Type,
|
|
Version: entity.SdkInfo.Version,
|
|
}
|
|
}
|
|
|
|
precedenceMap := map[string]uint32{}
|
|
for k, v := range entity.ServiceHostingPrecedences {
|
|
precedenceMap[k] = uint32(v)
|
|
}
|
|
|
|
costMap := map[string]uint32{}
|
|
for k, v := range entity.ServiceHostingCosts {
|
|
costMap[k] = uint32(v)
|
|
}
|
|
|
|
appData, err := json.Marshal(entity.AppData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msg := &edge_cmd_pb.Identity{
|
|
Id: entity.Id,
|
|
Name: entity.Name,
|
|
Tags: tags,
|
|
IdentityTypeId: entity.IdentityTypeId,
|
|
IsDefaultAdmin: entity.IsDefaultAdmin,
|
|
IsAdmin: entity.IsAdmin,
|
|
RoleAttributes: entity.RoleAttributes,
|
|
EnvInfo: envInfo,
|
|
SdkInfo: sdkInfo,
|
|
DefaultHostingPrecedence: uint32(entity.DefaultHostingPrecedence),
|
|
DefaultHostingCost: uint32(entity.DefaultHostingCost),
|
|
ServiceHostingPrecedences: precedenceMap,
|
|
ServiceHostingCosts: costMap,
|
|
AppData: appData,
|
|
AuthPolicyId: entity.AuthPolicyId,
|
|
ExternalId: entity.ExternalId,
|
|
Disabled: entity.Disabled,
|
|
DisabledAt: timePtrToPb(entity.DisabledAt),
|
|
DisabledUntil: timePtrToPb(entity.DisabledUntil),
|
|
Permissions: entity.Permissions,
|
|
}
|
|
|
|
for serviceId, configInfo := range entity.ServiceConfigs {
|
|
for configTypeId, configId := range configInfo {
|
|
msg.ServiceConfigs = append(msg.ServiceConfigs, &edge_cmd_pb.Identity_ServiceConfig{
|
|
ServiceId: serviceId,
|
|
ConfigTypeId: configTypeId,
|
|
ConfigId: configId,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, intf := range entity.Interfaces {
|
|
msg.Interfaces = append(msg.Interfaces, &edge_cmd_pb.Interface{
|
|
Name: intf.Name,
|
|
HardwareAddress: intf.HardwareAddress,
|
|
Mtu: intf.MTU,
|
|
Index: intf.Index,
|
|
Flags: intf.Flags,
|
|
Addresses: intf.Addresses,
|
|
})
|
|
}
|
|
|
|
return msg, nil
|
|
}
|
|
|
|
func (self *IdentityManager) UpdateSdkEnvInfo(identity *Identity, envInfo *EnvInfo, sdkInfo *SdkInfo, changeCtx *change.Context) error {
|
|
updateFields := fields.UpdatedFieldsMap{}
|
|
|
|
if identity.EnvInfo == nil {
|
|
identity.EnvInfo = &EnvInfo{}
|
|
}
|
|
|
|
if identity.SdkInfo == nil {
|
|
identity.SdkInfo = &SdkInfo{}
|
|
}
|
|
|
|
if envInfo != nil && !identity.EnvInfo.Equals(envInfo) {
|
|
updateFields.AddFields(db.FieldIdentityEnvInfoOs,
|
|
db.FieldIdentityEnvInfoOsRelease,
|
|
db.FieldIdentityEnvInfoOsVersion,
|
|
db.FieldIdentityEnvInfoHostname,
|
|
db.FieldIdentityEnvInfoArch,
|
|
db.FieldIdentityEnvInfoDomain)
|
|
identity.EnvInfo = envInfo
|
|
}
|
|
|
|
if sdkInfo != nil && !identity.SdkInfo.Equals(sdkInfo) {
|
|
updateFields.AddFields(db.FieldIdentitySdkInfoRevision,
|
|
db.FieldIdentitySdkInfoVersion,
|
|
db.FieldIdentitySdkInfoType,
|
|
db.FieldIdentitySdkInfoBranch,
|
|
db.FieldIdentitySdkInfoAppVersion,
|
|
db.FieldIdentitySdkInfoAppId)
|
|
identity.SdkInfo = sdkInfo
|
|
}
|
|
|
|
if len(updateFields) != 0 {
|
|
task := func() {
|
|
self.PatchInfo(identity, updateFields, changeCtx)
|
|
}
|
|
|
|
if !self.env.GetManagers().Command.backGroundableTask(task) {
|
|
pfxlog.Logger().WithField("identityId", identity.Id).Warn("background update queue is full, dropping identity env/sdk update")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *IdentityManager) Marshall(entity *Identity) ([]byte, error) {
|
|
msg, err := self.IdentityToProtobuf(entity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return proto.Marshal(msg)
|
|
}
|
|
|
|
func (self *IdentityManager) ProtobufToIdentity(msg *edge_cmd_pb.Identity) (*Identity, error) {
|
|
var envInfo *EnvInfo
|
|
if msg.EnvInfo != nil {
|
|
envInfo = &EnvInfo{
|
|
Arch: msg.EnvInfo.Arch,
|
|
Os: msg.EnvInfo.Os,
|
|
OsRelease: msg.EnvInfo.OsRelease,
|
|
OsVersion: msg.EnvInfo.OsVersion,
|
|
Domain: msg.EnvInfo.Domain,
|
|
Hostname: msg.EnvInfo.Hostname,
|
|
}
|
|
}
|
|
|
|
var sdkInfo *SdkInfo
|
|
if msg.SdkInfo != nil {
|
|
sdkInfo = &SdkInfo{
|
|
AppId: msg.SdkInfo.AppId,
|
|
AppVersion: msg.SdkInfo.AppVersion,
|
|
Branch: msg.SdkInfo.Branch,
|
|
Revision: msg.SdkInfo.Revision,
|
|
Type: msg.SdkInfo.Type,
|
|
Version: msg.SdkInfo.Version,
|
|
}
|
|
}
|
|
|
|
precedenceMap := map[string]ziti.Precedence{}
|
|
for k, v := range msg.ServiceHostingPrecedences {
|
|
precedenceMap[k] = ziti.Precedence(v)
|
|
}
|
|
|
|
costMap := map[string]uint16{}
|
|
for k, v := range msg.ServiceHostingCosts {
|
|
costMap[k] = uint16(v)
|
|
}
|
|
|
|
appData := map[string]interface{}{}
|
|
if err := json.Unmarshal(msg.AppData, &appData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var serviceConfigs map[string]map[string]string
|
|
|
|
for _, serviceConfig := range msg.ServiceConfigs {
|
|
if serviceConfigs == nil {
|
|
serviceConfigs = map[string]map[string]string{}
|
|
}
|
|
|
|
serviceMap, ok := serviceConfigs[serviceConfig.ServiceId]
|
|
if !ok {
|
|
serviceMap = map[string]string{}
|
|
serviceConfigs[serviceConfig.ServiceId] = serviceMap
|
|
}
|
|
serviceMap[serviceConfig.ConfigTypeId] = serviceConfig.ConfigId
|
|
}
|
|
|
|
result := &Identity{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: msg.Id,
|
|
Tags: edge_cmd_pb.DecodeTags(msg.Tags),
|
|
},
|
|
Name: msg.Name,
|
|
IdentityTypeId: msg.IdentityTypeId,
|
|
IsDefaultAdmin: msg.IsDefaultAdmin,
|
|
IsAdmin: msg.IsAdmin,
|
|
RoleAttributes: msg.RoleAttributes,
|
|
EnvInfo: envInfo,
|
|
SdkInfo: sdkInfo,
|
|
DefaultHostingPrecedence: ziti.Precedence(msg.DefaultHostingPrecedence),
|
|
DefaultHostingCost: uint16(msg.DefaultHostingCost),
|
|
ServiceHostingPrecedences: precedenceMap,
|
|
ServiceHostingCosts: costMap,
|
|
AppData: appData,
|
|
AuthPolicyId: msg.AuthPolicyId,
|
|
ExternalId: msg.ExternalId,
|
|
Disabled: msg.Disabled,
|
|
DisabledAt: pbTimeToTimePtr(msg.DisabledAt),
|
|
DisabledUntil: pbTimeToTimePtr(msg.DisabledUntil),
|
|
ServiceConfigs: serviceConfigs,
|
|
Permissions: msg.Permissions,
|
|
}
|
|
|
|
for _, intf := range msg.Interfaces {
|
|
result.Interfaces = append(result.Interfaces, &Interface{
|
|
Name: intf.Name,
|
|
HardwareAddress: intf.HardwareAddress,
|
|
MTU: intf.Mtu,
|
|
Index: intf.Index,
|
|
Flags: intf.Flags,
|
|
Addresses: intf.Addresses,
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (self *IdentityManager) Unmarshall(bytes []byte) (*Identity, error) {
|
|
msg := &edge_cmd_pb.Identity{}
|
|
if err := proto.Unmarshal(bytes, msg); err != nil {
|
|
return nil, err
|
|
}
|
|
return self.ProtobufToIdentity(msg)
|
|
}
|
|
|
|
type CreateIdentityWithEnrollmentsCmd struct {
|
|
manager *IdentityManager
|
|
identity *Identity
|
|
enrollments []*Enrollment
|
|
ctx *change.Context
|
|
}
|
|
|
|
func (self *CreateIdentityWithEnrollmentsCmd) Apply(ctx boltz.MutateContext) error {
|
|
return self.manager.ApplyCreateWithEnrollments(self, ctx)
|
|
}
|
|
|
|
func (self *CreateIdentityWithEnrollmentsCmd) Encode() ([]byte, error) {
|
|
identityMsg, err := self.manager.IdentityToProtobuf(self.identity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := &edge_cmd_pb.CreateIdentityWithEnrollmentsCmd{
|
|
Identity: identityMsg,
|
|
Ctx: ContextToProtobuf(self.ctx),
|
|
}
|
|
|
|
for _, enrollment := range self.enrollments {
|
|
enrollmentMsg, err := self.manager.GetEnv().GetManagers().Enrollment.EnrollmentToProtobuf(enrollment)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.Enrollments = append(cmd.Enrollments, enrollmentMsg)
|
|
}
|
|
cmd.GetCommandType()
|
|
return cmd_pb.EncodeProtobuf(cmd)
|
|
}
|
|
|
|
func (self *CreateIdentityWithEnrollmentsCmd) Decode(env Env, msg *edge_cmd_pb.CreateIdentityWithEnrollmentsCmd) error {
|
|
self.manager = env.GetManagers().Identity
|
|
|
|
identity, err := self.manager.ProtobufToIdentity(msg.Identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
self.identity = identity
|
|
self.ctx = ProtobufToContext(msg.Ctx)
|
|
for _, enrollmentMsg := range msg.Enrollments {
|
|
enrollment, err := self.manager.GetEnv().GetManagers().Enrollment.ProtobufToEnrollment(enrollmentMsg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
self.enrollments = append(self.enrollments, enrollment)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *CreateIdentityWithEnrollmentsCmd) GetChangeContext() *change.Context {
|
|
return self.ctx
|
|
}
|
|
|
|
type CreateIdentityWithAuthenticatorsCmd struct {
|
|
manager *IdentityManager
|
|
identity *Identity
|
|
authenticators []*Authenticator
|
|
ctx *change.Context
|
|
}
|
|
|
|
func (self *CreateIdentityWithAuthenticatorsCmd) Apply(ctx boltz.MutateContext) error {
|
|
return self.manager.ApplyCreateWithAuthenticators(self, ctx)
|
|
}
|
|
|
|
func (self *CreateIdentityWithAuthenticatorsCmd) Encode() ([]byte, error) {
|
|
identityMsg, err := self.manager.IdentityToProtobuf(self.identity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cmd := &edge_cmd_pb.CreateIdentityWithAuthenticatorsCmd{
|
|
Identity: identityMsg,
|
|
Ctx: ContextToProtobuf(self.ctx),
|
|
}
|
|
|
|
for _, authenticator := range self.authenticators {
|
|
authenticatorMsg, err := self.manager.GetEnv().GetManagers().Authenticator.AuthenticatorToProtobuf(authenticator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.Authenticators = append(cmd.Authenticators, authenticatorMsg)
|
|
}
|
|
|
|
return cmd_pb.EncodeProtobuf(cmd)
|
|
}
|
|
|
|
func (self *CreateIdentityWithAuthenticatorsCmd) Decode(env Env, msg *edge_cmd_pb.CreateIdentityWithAuthenticatorsCmd) error {
|
|
self.manager = env.GetManagers().Identity
|
|
|
|
identity, err := self.manager.ProtobufToIdentity(msg.Identity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
self.identity = identity
|
|
self.ctx = ProtobufToContext(msg.Ctx)
|
|
for _, authenticatorMsg := range msg.Authenticators {
|
|
enrollment, err := self.manager.GetEnv().GetManagers().Authenticator.ProtobufToAuthenticator(authenticatorMsg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
self.authenticators = append(self.authenticators, enrollment)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *CreateIdentityWithAuthenticatorsCmd) GetChangeContext() *change.Context {
|
|
return self.ctx
|
|
}
|
|
|
|
type identityStatusMap struct {
|
|
identityIdToErConStatus cmap.ConcurrentMap[string, *status]
|
|
initOnce sync.Once
|
|
activeDuration time.Duration
|
|
}
|
|
|
|
type status struct {
|
|
expiresAt time.Time
|
|
}
|
|
|
|
func newIdentityStatusMap(activeDuration time.Duration) *identityStatusMap {
|
|
return &identityStatusMap{
|
|
identityIdToErConStatus: cmap.New[*status](),
|
|
activeDuration: activeDuration,
|
|
}
|
|
}
|
|
|
|
func (statusMap *identityStatusMap) SetHasEdgeRouterConnection(identityId string) {
|
|
statusMap.initOnce.Do(statusMap.start)
|
|
|
|
statusMap.identityIdToErConStatus.Set(identityId, &status{
|
|
expiresAt: time.Now().Add(statusMap.activeDuration),
|
|
})
|
|
}
|
|
|
|
func (statusMap *identityStatusMap) HasEdgeRouterConnection(identityId string) bool {
|
|
if stat, ok := statusMap.identityIdToErConStatus.Get(identityId); ok {
|
|
now := time.Now()
|
|
ret := stat.expiresAt.After(now)
|
|
pfxlog.Logger().
|
|
WithField("identityId", identityId).
|
|
WithField("expiresAt", stat.expiresAt).
|
|
WithField("now", now).
|
|
Tracef("reporting identity from active ER conn pool: timedout")
|
|
return ret
|
|
}
|
|
|
|
pfxlog.Logger().
|
|
WithField("identityId", identityId).
|
|
Tracef("reporting identity from active ER conn pool: not found")
|
|
return false
|
|
}
|
|
|
|
func (statusMap *identityStatusMap) start() {
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
go func() {
|
|
for range ticker.C {
|
|
var toRemove []string
|
|
now := time.Now()
|
|
statusMap.identityIdToErConStatus.IterCb(func(key string, stat *status) {
|
|
if stat.expiresAt.Before(now) {
|
|
pfxlog.Logger().
|
|
WithField("identityId", key).
|
|
WithField("expiresAt", stat.expiresAt).
|
|
WithField("now", now).
|
|
Debugf("removing identity from active ER conn pool: not found")
|
|
toRemove = append(toRemove, key)
|
|
}
|
|
})
|
|
|
|
for _, identityId := range toRemove {
|
|
|
|
statusMap.identityIdToErConStatus.Remove(identityId)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
type IdentityOnlineState uint32
|
|
|
|
func (self IdentityOnlineState) String() string {
|
|
if self == IdentityStateOffline {
|
|
return "offline"
|
|
}
|
|
if self == IdentityStateOnline {
|
|
return "online"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
const (
|
|
IdentityStateOffline IdentityOnlineState = 0
|
|
IdentityStateOnline IdentityOnlineState = 1
|
|
IdentityStateUnknown IdentityOnlineState = 2
|
|
)
|
|
|
|
type identityConnections struct {
|
|
sync.RWMutex
|
|
routers map[string]ctrlchan.CtrlChannel
|
|
lastReportedState IdentityOnlineState
|
|
}
|
|
|
|
func (self *identityConnections) calculateState() IdentityOnlineState {
|
|
// if any router is connected, the identity is online
|
|
for _, router := range self.routers {
|
|
if !router.IsClosed() {
|
|
return IdentityStateOnline
|
|
}
|
|
}
|
|
|
|
// if the identity is reported as connected to one or more routers, but they're all offline,
|
|
// then the identity state is unknown
|
|
if len(self.routers) > 0 {
|
|
return IdentityStateUnknown
|
|
}
|
|
|
|
// if the identity has no router connections, it's off-line
|
|
return IdentityStateOffline
|
|
}
|
|
|
|
func newConnectionTracker(env Env) *ConnectionTracker {
|
|
result := &ConnectionTracker{
|
|
connections: cmap.New[*identityConnections](),
|
|
eventDispatcher: env.GetEventDispatcher(),
|
|
scanInterval: env.GetConfig().Edge.IdentityStatusConfig.ScanInterval,
|
|
unknownTimeout: env.GetConfig().Edge.IdentityStatusConfig.UnknownTimeout,
|
|
closeNotify: env.GetCloseNotifyChannel(),
|
|
}
|
|
if result.scanInterval < 5*time.Second {
|
|
result.scanInterval = 5 * time.Second
|
|
}
|
|
go result.runScanLoop()
|
|
return result
|
|
}
|
|
|
|
type ConnectionTracker struct {
|
|
connections cmap.ConcurrentMap[string, *identityConnections]
|
|
scanInterval time.Duration
|
|
unknownTimeout time.Duration
|
|
eventDispatcher event.Dispatcher
|
|
closeNotify <-chan struct{}
|
|
}
|
|
|
|
func (self *ConnectionTracker) runScanLoop() {
|
|
ticker := time.NewTicker(self.scanInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
self.ScanForDisconnectedRouters()
|
|
case <-self.closeNotify:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *ConnectionTracker) ScanForDisconnectedRouters() {
|
|
for entry := range self.connections.IterBuffered() {
|
|
var toRemove []ctrlchan.CtrlChannel
|
|
entry.Val.RLock()
|
|
for _, routerCh := range entry.Val.routers {
|
|
if routerCh.IsClosed() && routerCh.GetChannel().GetTimeSinceLastRead() > self.unknownTimeout {
|
|
toRemove = append(toRemove, routerCh)
|
|
}
|
|
}
|
|
entry.Val.RUnlock()
|
|
|
|
for _, routerCh := range toRemove {
|
|
self.MarkDisconnected(entry.Key, routerCh)
|
|
}
|
|
|
|
if len(toRemove) == 0 {
|
|
var reportState *IdentityOnlineState
|
|
|
|
entry.Val.Lock()
|
|
lastReportedState := entry.Val.lastReportedState
|
|
currentState := entry.Val.calculateState()
|
|
if lastReportedState != currentState {
|
|
reportState = ¤tState
|
|
}
|
|
entry.Val.Unlock()
|
|
|
|
if reportState != nil {
|
|
self.SendSdkOnlineStatusChangeEvent(entry.Key, *reportState)
|
|
}
|
|
}
|
|
|
|
entry.Val.Lock()
|
|
if len(entry.Val.routers) == 0 {
|
|
self.connections.RemoveCb(entry.Key, func(key string, v *identityConnections, exists bool) bool {
|
|
if v != nil {
|
|
return len(v.routers) == 0
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
entry.Val.Unlock()
|
|
}
|
|
}
|
|
|
|
func (self *ConnectionTracker) MarkConnected(identityId string, ch ctrlchan.CtrlChannel) {
|
|
pfxlog.Logger().WithField("identityId", identityId).WithField("routerId", ch.PeerId()).Trace("marking identity connected to router")
|
|
var postUpsertCallback func()
|
|
self.connections.Upsert(identityId, nil, func(exist bool, valueInMap *identityConnections, newValue *identityConnections) *identityConnections {
|
|
if valueInMap == nil {
|
|
valueInMap = &identityConnections{
|
|
routers: map[string]ctrlchan.CtrlChannel{},
|
|
}
|
|
}
|
|
|
|
if ch.IsClosed() {
|
|
return valueInMap
|
|
}
|
|
|
|
valueInMap.Lock()
|
|
oldState := valueInMap.calculateState()
|
|
valueInMap.routers[ch.PeerId()] = ch
|
|
newState := valueInMap.calculateState()
|
|
lastReportedState := valueInMap.lastReportedState
|
|
valueInMap.lastReportedState = newState
|
|
valueInMap.Unlock()
|
|
|
|
if newState != oldState || newState != lastReportedState {
|
|
postUpsertCallback = func() {
|
|
self.SendSdkOnlineStatusChangeEvent(identityId, newState)
|
|
}
|
|
}
|
|
return valueInMap
|
|
})
|
|
|
|
if postUpsertCallback != nil {
|
|
postUpsertCallback()
|
|
}
|
|
}
|
|
|
|
func (self *ConnectionTracker) MarkDisconnected(identityId string, ch ctrlchan.CtrlChannel) {
|
|
pfxlog.Logger().WithField("identityId", identityId).WithField("routerId", ch.PeerId()).Trace("marking identity disconnected from router")
|
|
var postUpsertCallback func()
|
|
self.connections.Upsert(identityId, nil, func(exist bool, valueInMap *identityConnections, newValue *identityConnections) *identityConnections {
|
|
if valueInMap == nil {
|
|
return &identityConnections{
|
|
routers: map[string]ctrlchan.CtrlChannel{},
|
|
}
|
|
}
|
|
|
|
valueInMap.Lock()
|
|
oldState := valueInMap.calculateState()
|
|
|
|
current := valueInMap.routers[ch.PeerId()]
|
|
if current == nil || current == ch || current.IsClosed() {
|
|
delete(valueInMap.routers, ch.PeerId())
|
|
}
|
|
|
|
newState := valueInMap.calculateState()
|
|
lastReportedState := valueInMap.lastReportedState
|
|
valueInMap.lastReportedState = newState
|
|
valueInMap.Unlock()
|
|
|
|
if newState != oldState || newState != lastReportedState {
|
|
postUpsertCallback = func() {
|
|
self.SendSdkOnlineStatusChangeEvent(identityId, newState)
|
|
}
|
|
}
|
|
return valueInMap
|
|
})
|
|
|
|
if postUpsertCallback != nil {
|
|
postUpsertCallback()
|
|
}
|
|
}
|
|
|
|
func (self *ConnectionTracker) SendSdkOnlineStatusChangeEvent(identityId string, state IdentityOnlineState) {
|
|
var eventType event.SdkEventType
|
|
if state == IdentityStateOffline {
|
|
eventType = event.SdkOffline
|
|
} else if state == IdentityStateOnline {
|
|
eventType = event.SdkOnline
|
|
} else if state == IdentityStateUnknown {
|
|
eventType = event.SdkStatusUnknown
|
|
}
|
|
|
|
self.eventDispatcher.AcceptSdkEvent(&event.SdkEvent{
|
|
Namespace: event.SdkEventNS,
|
|
EventType: eventType,
|
|
Timestamp: time.Now(),
|
|
IdentityId: identityId,
|
|
})
|
|
}
|
|
|
|
func (self *ConnectionTracker) GetIdentityOnlineState(identityId string) IdentityOnlineState {
|
|
val, _ := self.connections.Get(identityId)
|
|
if val == nil {
|
|
return IdentityStateOffline
|
|
}
|
|
val.RLock()
|
|
defer val.RUnlock()
|
|
return val.calculateState()
|
|
}
|
|
|
|
func (self *ConnectionTracker) SyncAllFromRouter(state *edge_ctrl_pb.ConnectEvents, ch ctrlchan.CtrlChannel) {
|
|
m := map[string]bool{}
|
|
for _, identityState := range state.Events {
|
|
m[identityState.IdentityId] = identityState.IsConnected
|
|
if identityState.IsConnected {
|
|
self.MarkConnected(identityState.IdentityId, ch)
|
|
}
|
|
}
|
|
|
|
for _, identityId := range self.connections.Keys() {
|
|
// note: don't mark router itself as disconnected
|
|
if connected := m[identityId]; !connected && identityId != ch.PeerId() {
|
|
self.MarkDisconnected(identityId, ch)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *ConnectionTracker) Inspect() *inspect.CtrlIdentityConnections {
|
|
result := &inspect.CtrlIdentityConnections{
|
|
Connections: map[string]*inspect.CtrlIdentityConnectionDetail{},
|
|
ScanInterval: self.scanInterval.String(),
|
|
}
|
|
|
|
for entry := range self.connections.IterBuffered() {
|
|
entry.Val.Lock()
|
|
val := &inspect.CtrlIdentityConnectionDetail{
|
|
ConnectedRouters: map[string]*inspect.CtrlRouterConnection{},
|
|
LastReportedState: entry.Val.lastReportedState.String(),
|
|
}
|
|
result.Connections[entry.Key] = val
|
|
for routerId, ch := range entry.Val.routers {
|
|
val.ConnectedRouters[routerId] = &inspect.CtrlRouterConnection{
|
|
RouterId: ch.PeerId(),
|
|
Closed: ch.IsClosed(),
|
|
TimeSinceLastWrite: ch.GetChannel().GetTimeSinceLastRead().String(),
|
|
}
|
|
}
|
|
entry.Val.Unlock()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
type UpdateServiceConfigsCmd struct {
|
|
manager *IdentityManager
|
|
identityId string
|
|
add bool
|
|
serviceConfigs []ServiceConfig
|
|
ctx *change.Context
|
|
}
|
|
|
|
func (self *UpdateServiceConfigsCmd) Apply(ctx boltz.MutateContext) error {
|
|
return self.manager.ApplyUpdateServiceConfigs(self, ctx)
|
|
}
|
|
|
|
func (self *UpdateServiceConfigsCmd) Encode() ([]byte, error) {
|
|
cmd := &edge_cmd_pb.UpdateServiceConfigsCmd{
|
|
IdentityId: self.identityId,
|
|
Add: self.add,
|
|
Ctx: ContextToProtobuf(self.ctx),
|
|
}
|
|
|
|
for _, serviceConfig := range self.serviceConfigs {
|
|
cmd.ServiceConfigs = append(cmd.ServiceConfigs, &edge_cmd_pb.UpdateServiceConfigsCmd_ServiceConfig{
|
|
ServiceId: serviceConfig.Service,
|
|
ConfigId: serviceConfig.Config,
|
|
})
|
|
}
|
|
|
|
return cmd_pb.EncodeProtobuf(cmd)
|
|
}
|
|
|
|
func (self *UpdateServiceConfigsCmd) Decode(env Env, msg *edge_cmd_pb.UpdateServiceConfigsCmd) error {
|
|
self.manager = env.GetManagers().Identity
|
|
|
|
self.identityId = msg.IdentityId
|
|
self.add = msg.Add
|
|
self.ctx = ProtobufToContext(msg.Ctx)
|
|
|
|
for _, serviceConfig := range msg.ServiceConfigs {
|
|
self.serviceConfigs = append(self.serviceConfigs, ServiceConfig{
|
|
Service: serviceConfig.ServiceId,
|
|
Config: serviceConfig.ConfigId,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *UpdateServiceConfigsCmd) GetChangeContext() *change.Context {
|
|
return self.ctx
|
|
}
|