mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-23 00:00:52 +00:00
147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
package terminal_session
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/core/global"
|
|
)
|
|
|
|
type RevokeFunc func(scope, userID, authSessionID string) error
|
|
|
|
type resumableRevocation struct {
|
|
err error
|
|
retry RevokeFunc
|
|
}
|
|
|
|
func (e resumableRevocation) Error() string { return e.err.Error() }
|
|
func (e resumableRevocation) Unwrap() error { return e.err }
|
|
func (e resumableRevocation) RetryRevokeFunc() RevokeFunc { return e.retry }
|
|
|
|
func PendingRevocation(err error, retry RevokeFunc) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return resumableRevocation{err: err, retry: retry}
|
|
}
|
|
|
|
func nextRevokeAttempt(err error, fallback RevokeFunc) RevokeFunc {
|
|
var resumable interface{ RetryRevokeFunc() RevokeFunc }
|
|
if errors.As(err, &resumable) && resumable.RetryRevokeFunc() != nil {
|
|
return resumable.RetryRevokeFunc()
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
type revokeTarget struct{ scope, userID, authSessionID string }
|
|
type revokeRetry struct {
|
|
target revokeTarget
|
|
revoke RevokeFunc
|
|
next time.Time
|
|
deadline time.Time
|
|
attempt int
|
|
version uint64
|
|
}
|
|
|
|
type revocationQueue struct {
|
|
mu sync.Mutex
|
|
pending map[revokeTarget]revokeRetry
|
|
limit int
|
|
version uint64
|
|
}
|
|
|
|
var terminalRevocations = revocationQueue{pending: make(map[revokeTarget]revokeRetry), limit: 256}
|
|
var startRevocationWorker sync.Once
|
|
|
|
func RevokeWithRetry(scope, userID, authSessionID string, revoke RevokeFunc) error {
|
|
if revoke == nil || (scope != "all" && userID == "") || (scope == "auth_session" && authSessionID == "") ||
|
|
(scope != "all" && scope != "user" && scope != "auth_session") {
|
|
return errors.New("invalid terminal revocation")
|
|
}
|
|
target := revokeTarget{scope, userID, authSessionID}
|
|
terminalRevocations.mu.Lock()
|
|
previous, hadPrevious := terminalRevocations.pending[target]
|
|
terminalRevocations.mu.Unlock()
|
|
err := revoke(scope, userID, authSessionID)
|
|
if err == nil {
|
|
if hadPrevious {
|
|
terminalRevocations.complete(previous, nil, time.Now())
|
|
}
|
|
return nil
|
|
}
|
|
if !terminalRevocations.enqueue(target, nextRevokeAttempt(err, revoke), time.Now()) {
|
|
err = errors.Join(err, errors.New("terminal revocation retry capacity exceeded"))
|
|
} else {
|
|
startRevocationWorker.Do(func() { go runRevocationRetries() })
|
|
}
|
|
logRevokeFailure(target, err)
|
|
return fmt.Errorf("terminal closure is pending: %w", err)
|
|
}
|
|
|
|
func (q *revocationQueue) enqueue(target revokeTarget, revoke RevokeFunc, now time.Time) bool {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
if _, exists := q.pending[target]; !exists && len(q.pending) >= q.limit {
|
|
return false
|
|
}
|
|
q.version++
|
|
q.pending[target] = revokeRetry{target: target, revoke: revoke, next: now.Add(5 * time.Second), deadline: now.Add(2 * time.Minute), version: q.version}
|
|
return true
|
|
}
|
|
|
|
func (q *revocationQueue) due(now time.Time) []revokeRetry {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
var jobs []revokeRetry
|
|
for _, item := range q.pending {
|
|
if !now.Before(item.next) {
|
|
jobs = append(jobs, item)
|
|
}
|
|
}
|
|
return jobs
|
|
}
|
|
|
|
func (q *revocationQueue) complete(item revokeRetry, err error, now time.Time) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
current, exists := q.pending[item.target]
|
|
if !exists || current.version != item.version {
|
|
return
|
|
}
|
|
if err == nil || !now.Before(item.deadline) {
|
|
delete(q.pending, item.target)
|
|
return
|
|
}
|
|
item.attempt++
|
|
item.revoke = nextRevokeAttempt(err, item.revoke)
|
|
item.next = now.Add(time.Duration(min(item.attempt+1, 6)) * 5 * time.Second)
|
|
q.pending[item.target] = item
|
|
}
|
|
|
|
func runRevocationRetries() {
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
for now := range ticker.C {
|
|
for _, item := range terminalRevocations.due(now) {
|
|
if time.Now().After(item.deadline) {
|
|
terminalRevocations.complete(item, errors.New("revocation retry deadline reached"), time.Now())
|
|
logRevokeFailure(item.target, errors.New("revocation retries exhausted; Agent authorization lease must expire"))
|
|
continue
|
|
}
|
|
err := item.revoke(item.target.scope, item.target.userID, item.target.authSessionID)
|
|
terminalRevocations.complete(item, err, time.Now())
|
|
if err != nil {
|
|
logRevokeFailure(item.target, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func logRevokeFailure(target revokeTarget, err error) {
|
|
if global.LOG != nil {
|
|
global.LOG.Warnf("terminal revocation scope=%s user=%s session=%s: %v", target.scope, target.userID, target.authSessionID, err)
|
|
}
|
|
}
|