216 lines
4.6 KiB
Go
216 lines
4.6 KiB
Go
package apiapproval
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.julianfamily.org/keepassgo/apitokens"
|
|
)
|
|
|
|
var (
|
|
ErrRequestDenied = errors.New("authorization request denied")
|
|
ErrRequestCanceled = errors.New("authorization request canceled")
|
|
ErrRequestTimedOut = errors.New("authorization request timed out")
|
|
ErrRequestNotFound = errors.New("authorization request not found")
|
|
)
|
|
|
|
type Outcome string
|
|
|
|
const (
|
|
OutcomeAllowOnce Outcome = "allow-once"
|
|
OutcomeDenyOnce Outcome = "deny-once"
|
|
OutcomeAllowPermanent Outcome = "allow-permanent"
|
|
OutcomeDenyPermanent Outcome = "deny-permanent"
|
|
OutcomeCancel Outcome = "cancel"
|
|
)
|
|
|
|
type Request struct {
|
|
ID string
|
|
TokenID string
|
|
TokenName string
|
|
ClientName string
|
|
Operation apitokens.Operation
|
|
Resource apitokens.Resource
|
|
RequestedAt time.Time
|
|
}
|
|
|
|
type Result struct {
|
|
Outcome Outcome
|
|
Rule *apitokens.PolicyRule
|
|
}
|
|
|
|
type Broker struct {
|
|
mu sync.Mutex
|
|
pending map[string]*pendingRequest
|
|
timeout time.Duration
|
|
now func() time.Time
|
|
nextID func() string
|
|
}
|
|
|
|
type pendingRequest struct {
|
|
request Request
|
|
done chan Outcome
|
|
}
|
|
|
|
type idGenerator struct {
|
|
mu sync.Mutex
|
|
counter int
|
|
}
|
|
|
|
func NewBroker(timeout time.Duration) *Broker {
|
|
gen := &idGenerator{}
|
|
return &Broker{
|
|
pending: map[string]*pendingRequest{},
|
|
timeout: timeout,
|
|
now: func() time.Time {
|
|
return time.Now().UTC()
|
|
},
|
|
nextID: func() string {
|
|
return gen.Next()
|
|
},
|
|
}
|
|
}
|
|
|
|
func (g *idGenerator) Next() string {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.counter++
|
|
return "approval-" + strconv.Itoa(g.counter)
|
|
}
|
|
|
|
func (b *Broker) Pending() []Request {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
requests := make([]Request, 0, len(b.pending))
|
|
for _, pending := range b.pending {
|
|
requests = append(requests, pending.request)
|
|
}
|
|
slices.SortFunc(requests, func(a, c Request) int {
|
|
switch {
|
|
case a.RequestedAt.Before(c.RequestedAt):
|
|
return -1
|
|
case a.RequestedAt.After(c.RequestedAt):
|
|
return 1
|
|
case a.ID < c.ID:
|
|
return -1
|
|
case a.ID > c.ID:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
})
|
|
return requests
|
|
}
|
|
|
|
func (b *Broker) Request(ctx context.Context, token apitokens.Token, op apitokens.Operation, resource apitokens.Resource) (Result, error) {
|
|
if b == nil {
|
|
return Result{}, ErrRequestTimedOut
|
|
}
|
|
|
|
pending := &pendingRequest{
|
|
request: Request{
|
|
ID: b.nextID(),
|
|
TokenID: token.ID,
|
|
TokenName: token.Name,
|
|
ClientName: token.ClientName,
|
|
Operation: op,
|
|
Resource: resource,
|
|
RequestedAt: b.now(),
|
|
},
|
|
done: make(chan Outcome, 1),
|
|
}
|
|
|
|
b.mu.Lock()
|
|
b.pending[pending.request.ID] = pending
|
|
b.mu.Unlock()
|
|
|
|
defer func() {
|
|
b.mu.Lock()
|
|
delete(b.pending, pending.request.ID)
|
|
b.mu.Unlock()
|
|
}()
|
|
|
|
timer := time.NewTimer(b.timeout)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case outcome := <-pending.done:
|
|
result, err := resultForOutcome(pending.request, outcome)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
return result, nil
|
|
case <-timer.C:
|
|
return Result{}, ErrRequestTimedOut
|
|
case <-ctx.Done():
|
|
return Result{}, ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (b *Broker) Resolve(id string, outcome Outcome) (Request, *apitokens.PolicyRule, error) {
|
|
b.mu.Lock()
|
|
pending, ok := b.pending[id]
|
|
b.mu.Unlock()
|
|
if !ok {
|
|
return Request{}, nil, ErrRequestNotFound
|
|
}
|
|
|
|
rule, err := RuleFromDecision(pending.request, outcome)
|
|
if err != nil {
|
|
return Request{}, nil, err
|
|
}
|
|
|
|
select {
|
|
case pending.done <- outcome:
|
|
default:
|
|
}
|
|
return pending.request, rule, nil
|
|
}
|
|
|
|
func resultForOutcome(request Request, outcome Outcome) (Result, error) {
|
|
rule, err := RuleFromDecision(request, outcome)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
result := Result{Outcome: outcome, Rule: rule}
|
|
switch outcome {
|
|
case OutcomeAllowOnce, OutcomeAllowPermanent:
|
|
return result, nil
|
|
case OutcomeDenyOnce, OutcomeDenyPermanent:
|
|
return result, ErrRequestDenied
|
|
case OutcomeCancel:
|
|
return result, ErrRequestCanceled
|
|
default:
|
|
return Result{}, fmt.Errorf("unsupported approval outcome %q", outcome)
|
|
}
|
|
}
|
|
|
|
func RuleFromDecision(request Request, outcome Outcome) (*apitokens.PolicyRule, error) {
|
|
switch outcome {
|
|
case OutcomeAllowPermanent:
|
|
rule := apitokens.PolicyRule{
|
|
Effect: apitokens.EffectAllow,
|
|
Operation: request.Operation,
|
|
Resource: request.Resource,
|
|
}
|
|
return &rule, nil
|
|
case OutcomeDenyPermanent:
|
|
rule := apitokens.PolicyRule{
|
|
Effect: apitokens.EffectDeny,
|
|
Operation: request.Operation,
|
|
Resource: request.Resource,
|
|
}
|
|
return &rule, nil
|
|
case OutcomeAllowOnce, OutcomeDenyOnce, OutcomeCancel:
|
|
return nil, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported approval outcome %q", outcome)
|
|
}
|
|
}
|