mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 08:45:41 +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)
319 lines
9.4 KiB
Go
319 lines
9.4 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 (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/openziti/ziti/v2/common/pb/cmd_pb"
|
|
"github.com/openziti/ziti/v2/common/pb/edge_cmd_pb"
|
|
"github.com/openziti/ziti/v2/controller/change"
|
|
"github.com/openziti/ziti/v2/controller/command"
|
|
"github.com/openziti/ziti/v2/controller/db"
|
|
"github.com/openziti/ziti/v2/controller/models"
|
|
"github.com/openziti/ziti/v2/controller/storage/boltz"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
func NewRevocationManager(env Env) *RevocationManager {
|
|
manager := &RevocationManager{
|
|
baseEntityManager: newBaseEntityManager[*Revocation, *db.Revocation](env, env.GetStores().Revocation),
|
|
}
|
|
manager.impl = manager
|
|
|
|
RegisterManagerDecoder[*Revocation](env, manager)
|
|
RegisterCommand(env, &DeleteRevocationsBatchCommand{}, &edge_cmd_pb.DeleteRevocationsBatchCommand{})
|
|
RegisterCommand(env, &CreateRevocationsBatchCommand{}, &edge_cmd_pb.CreateRevocationsBatchCommand{})
|
|
|
|
return manager
|
|
}
|
|
|
|
type RevocationManager struct {
|
|
baseEntityManager[*Revocation, *db.Revocation]
|
|
}
|
|
|
|
func (self *RevocationManager) ApplyUpdate(_ *command.UpdateEntityCommand[*Revocation], ctx boltz.MutateContext) error {
|
|
return errors.New("unsupported")
|
|
}
|
|
|
|
func (self *RevocationManager) Create(entity *Revocation, ctx *change.Context) error {
|
|
return DispatchCreate[*Revocation](self, entity, ctx)
|
|
}
|
|
|
|
// CreateOrReplace creates the revocation, first removing any existing revocation
|
|
// with the same id. Identity- and api-session-scoped revocations are keyed by
|
|
// the identity/session id, so a repeat (e.g. re-disabling an identity, or a
|
|
// second logout) would otherwise collide with the lingering prior entry;
|
|
// replacing it refreshes the ExpiresAt and IssuedBefore cutoff.
|
|
func (self *RevocationManager) CreateOrReplace(entity *Revocation, ctx *change.Context) error {
|
|
if existing, _ := self.Read(entity.Id); existing != nil {
|
|
if err := self.Delete(entity.Id, ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return self.Create(entity, ctx)
|
|
}
|
|
|
|
func (self *RevocationManager) ApplyCreate(cmd *command.CreateEntityCommand[*Revocation], ctx boltz.MutateContext) error {
|
|
_, err := self.createEntity(cmd.Entity, ctx)
|
|
return err
|
|
}
|
|
|
|
func (self *RevocationManager) NewModelEntity() *Revocation {
|
|
return &Revocation{}
|
|
}
|
|
|
|
const revocationDeleteBatchSize = 500
|
|
|
|
// DeleteExpired deletes all revocations whose ExpiresAt is in the past, working
|
|
// in batches of revocationDeleteBatchSize until no expired entries remain.
|
|
// Returns the total number of entries deleted.
|
|
func (self *RevocationManager) DeleteExpired(ctx *change.Context) (int, error) {
|
|
query := fmt.Sprintf(`expiresAt < datetime(%s) limit %d`, time.Now().UTC().Format(time.RFC3339), revocationDeleteBatchSize)
|
|
total := 0
|
|
for {
|
|
result, err := self.BaseList(query)
|
|
if err != nil {
|
|
return total, err
|
|
}
|
|
|
|
ids := make([]string, 0, len(result.GetEntities()))
|
|
for _, entity := range result.GetEntities() {
|
|
ids = append(ids, entity.GetId())
|
|
}
|
|
|
|
if len(ids) == 0 {
|
|
break
|
|
}
|
|
|
|
if err = self.DeleteBatch(ids, ctx); err != nil {
|
|
return total, err
|
|
}
|
|
total += len(ids)
|
|
|
|
if len(ids) < revocationDeleteBatchSize {
|
|
break
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func (self *RevocationManager) Read(id string) (*Revocation, error) {
|
|
modelEntity := &Revocation{}
|
|
if err := self.readEntity(id, modelEntity); err != nil {
|
|
return nil, err
|
|
}
|
|
return modelEntity, nil
|
|
}
|
|
|
|
func (self *RevocationManager) Marshall(entity *Revocation) ([]byte, error) {
|
|
tags, err := edge_cmd_pb.EncodeTags(entity.Tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msg := &edge_cmd_pb.Revocation{
|
|
Id: entity.Id,
|
|
ExpiresAt: timePtrToPb(&entity.ExpiresAt),
|
|
Tags: tags,
|
|
Type: entity.Type,
|
|
}
|
|
if !entity.IssuedBefore.IsZero() {
|
|
msg.IssuedBefore = timePtrToPb(&entity.IssuedBefore)
|
|
}
|
|
|
|
return proto.Marshal(msg)
|
|
}
|
|
|
|
func (self *RevocationManager) Unmarshall(bytes []byte) (*Revocation, error) {
|
|
msg := &edge_cmd_pb.Revocation{}
|
|
if err := proto.Unmarshal(bytes, msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if msg.ExpiresAt == nil {
|
|
return nil, fmt.Errorf("revocation msg for id '%v' has nil ExpiresAt", msg.Id)
|
|
}
|
|
|
|
revocation := &Revocation{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: msg.Id,
|
|
Tags: edge_cmd_pb.DecodeTags(msg.Tags),
|
|
},
|
|
ExpiresAt: *pbTimeToTimePtr(msg.ExpiresAt),
|
|
Type: msg.Type,
|
|
}
|
|
if msg.IssuedBefore != nil {
|
|
revocation.IssuedBefore = msg.IssuedBefore.AsTime()
|
|
}
|
|
return revocation, nil
|
|
}
|
|
|
|
// DeleteBatch dispatches a batched delete of revocation IDs through raft as a
|
|
// single log entry and a single DB transaction.
|
|
func (self *RevocationManager) DeleteBatch(ids []string, ctx *change.Context) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
cmd := &DeleteRevocationsBatchCommand{
|
|
Context: ctx,
|
|
Manager: self,
|
|
Ids: ids,
|
|
}
|
|
return self.Dispatch(cmd)
|
|
}
|
|
|
|
// ApplyDeleteBatch removes revocations by ID in a single DB transaction.
|
|
func (self *RevocationManager) ApplyDeleteBatch(cmd *DeleteRevocationsBatchCommand, ctx boltz.MutateContext) error {
|
|
var errorList []error
|
|
err := self.GetDb().Update(ctx, func(ctx boltz.MutateContext) error {
|
|
for _, id := range cmd.Ids {
|
|
if self.Store.IsEntityPresent(ctx.Tx(), id) {
|
|
if err := self.Store.DeleteById(ctx, id); err != nil {
|
|
errorList = append(errorList, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
errorList = append(errorList, err)
|
|
}
|
|
return errors.Join(errorList...)
|
|
}
|
|
|
|
// DeleteRevocationsBatchCommand deletes a batch of revocations through raft
|
|
// in a single log entry and a single DB transaction.
|
|
type DeleteRevocationsBatchCommand struct {
|
|
Context *change.Context
|
|
Manager *RevocationManager
|
|
Ids []string
|
|
}
|
|
|
|
func (self *DeleteRevocationsBatchCommand) Apply(ctx boltz.MutateContext) error {
|
|
return self.Manager.ApplyDeleteBatch(self, ctx)
|
|
}
|
|
|
|
func (self *DeleteRevocationsBatchCommand) Encode() ([]byte, error) {
|
|
return cmd_pb.EncodeProtobuf(&edge_cmd_pb.DeleteRevocationsBatchCommand{
|
|
EntityIds: self.Ids,
|
|
Ctx: ContextToProtobuf(self.Context),
|
|
})
|
|
}
|
|
|
|
func (self *DeleteRevocationsBatchCommand) Decode(env Env, msg *edge_cmd_pb.DeleteRevocationsBatchCommand) error {
|
|
self.Manager = env.GetManagers().Revocation
|
|
self.Ids = msg.EntityIds
|
|
self.Context = ProtobufToContext(msg.Ctx)
|
|
return nil
|
|
}
|
|
|
|
func (self *DeleteRevocationsBatchCommand) GetChangeContext() *change.Context {
|
|
return self.Context
|
|
}
|
|
|
|
// CreateBatch dispatches a batched create of revocations through raft as a
|
|
// single log entry and a single DB transaction.
|
|
func (self *RevocationManager) CreateBatch(revocations []*Revocation, ctx *change.Context) error {
|
|
if len(revocations) == 0 {
|
|
return nil
|
|
}
|
|
cmd := &CreateRevocationsBatchCommand{
|
|
Context: ctx,
|
|
Manager: self,
|
|
Revocations: revocations,
|
|
}
|
|
return self.Dispatch(cmd)
|
|
}
|
|
|
|
// ApplyCreateBatch persists a batch of revocations in a single DB transaction.
|
|
func (self *RevocationManager) ApplyCreateBatch(cmd *CreateRevocationsBatchCommand, ctx boltz.MutateContext) error {
|
|
return self.GetDb().Update(ctx, func(ctx boltz.MutateContext) error {
|
|
for _, rev := range cmd.Revocations {
|
|
if _, err := self.createEntityInTx(ctx, rev); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// CreateRevocationsBatchCommand creates a batch of revocations through raft
|
|
// in a single log entry and a single DB transaction.
|
|
type CreateRevocationsBatchCommand struct {
|
|
Context *change.Context
|
|
Manager *RevocationManager
|
|
Revocations []*Revocation
|
|
}
|
|
|
|
func (self *CreateRevocationsBatchCommand) Apply(ctx boltz.MutateContext) error {
|
|
return self.Manager.ApplyCreateBatch(self, ctx)
|
|
}
|
|
|
|
func (self *CreateRevocationsBatchCommand) Encode() ([]byte, error) {
|
|
var pbRevocations []*edge_cmd_pb.Revocation
|
|
for _, rev := range self.Revocations {
|
|
tags, err := edge_cmd_pb.EncodeTags(rev.Tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pbRev := &edge_cmd_pb.Revocation{
|
|
Id: rev.Id,
|
|
ExpiresAt: timePtrToPb(&rev.ExpiresAt),
|
|
Tags: tags,
|
|
Type: rev.Type,
|
|
}
|
|
if !rev.IssuedBefore.IsZero() {
|
|
pbRev.IssuedBefore = timePtrToPb(&rev.IssuedBefore)
|
|
}
|
|
pbRevocations = append(pbRevocations, pbRev)
|
|
}
|
|
return cmd_pb.EncodeProtobuf(&edge_cmd_pb.CreateRevocationsBatchCommand{
|
|
Revocations: pbRevocations,
|
|
Ctx: ContextToProtobuf(self.Context),
|
|
})
|
|
}
|
|
|
|
func (self *CreateRevocationsBatchCommand) Decode(env Env, msg *edge_cmd_pb.CreateRevocationsBatchCommand) error {
|
|
self.Manager = env.GetManagers().Revocation
|
|
self.Context = ProtobufToContext(msg.Ctx)
|
|
for _, pbRev := range msg.Revocations {
|
|
if pbRev.ExpiresAt == nil {
|
|
return fmt.Errorf("revocation batch entry for id '%v' has nil ExpiresAt", pbRev.Id)
|
|
}
|
|
rev := &Revocation{
|
|
BaseEntity: models.BaseEntity{
|
|
Id: pbRev.Id,
|
|
Tags: edge_cmd_pb.DecodeTags(pbRev.Tags),
|
|
},
|
|
ExpiresAt: *pbTimeToTimePtr(pbRev.ExpiresAt),
|
|
Type: pbRev.Type,
|
|
}
|
|
if pbRev.IssuedBefore != nil {
|
|
rev.IssuedBefore = pbRev.IssuedBefore.AsTime()
|
|
}
|
|
self.Revocations = append(self.Revocations, rev)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *CreateRevocationsBatchCommand) GetChangeContext() *change.Context {
|
|
return self.Context
|
|
}
|