dehub/accessctl/filter_sig.go

114 lines
3.0 KiB
Go
Raw Normal View History

package accessctl
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
)
// FilterSignature represents the configuration of a Filter which requires one
// or more signature credentials to be present on a commit.
//
// Either AccountIDs, AnyAccount, or Any must be filled in; all are mutually
// exclusive.
type FilterSignature struct {
AccountIDs []string `yaml:"account_ids,omitempty"`
Any bool `yaml:"any,omitempty"`
AnyAccount bool `yaml:"any_account,omitempty"`
Count string `yaml:"count,omitempty"`
}
var _ FilterInterface = FilterSignature{}
func (f FilterSignature) targetNum() (int, error) {
if f.Count == "" {
return 1, nil
} else if !strings.HasSuffix(f.Count, "%") {
return strconv.Atoi(f.Count)
} else if f.AnyAccount {
return 0, errors.New("cannot use AnyAccount and a percent Count together")
}
percentStr := strings.TrimRight(f.Count, "%")
percent, err := strconv.ParseFloat(percentStr, 64)
if err != nil {
return 0, fmt.Errorf("could not parse Count as percent %q: %w", f.Count, err)
}
target := float64(len(f.AccountIDs)) * percent / 100
target = math.Ceil(target)
return int(target), nil
}
// ErrFilterSignatureUnsatisfied is returned from FilterSignature's
// Match method when the filter has not been satisfied.
type ErrFilterSignatureUnsatisfied struct {
TargetNumAccounts, NumAccounts int
}
func (err ErrFilterSignatureUnsatisfied) Error() string {
return fmt.Sprintf("not enough valid signature credentials, filter requires %d but only had %d",
err.TargetNumAccounts, err.NumAccounts)
}
// MatchCommit returns true if the CommitRequest contains a sufficient number of
// signature Credentials.
func (f FilterSignature) MatchCommit(req CommitRequest) error {
targetN, err := f.targetNum()
if err != nil {
return fmt.Errorf("computing target number of accounts: %w", err)
}
var numSigs int
credAccountIDs := map[string]struct{}{}
for _, cred := range req.Credentials {
// TODO support other kinds of signatures
if cred.PGPSignature == nil {
continue
}
numSigs++
if cred.AccountID != "" {
credAccountIDs[cred.AccountID] = struct{}{}
}
}
if numSigs == 0 {
return ErrFilterNoMatch{
Err: ErrFilterSignatureUnsatisfied{TargetNumAccounts: targetN},
}
}
var n int
if f.Any {
return nil
} else if f.AnyAccount {
// TODO this doesn't actually check that the accounts are defined in the
// Config. It works for now as long as the Credentials are valid, since
// only an Account defined in the Config could create a valid
// Credential, but once that's not the case this will need to be
// revisited.
n = len(credAccountIDs)
} else {
targetAccountIDs := map[string]struct{}{}
for _, accountID := range f.AccountIDs {
targetAccountIDs[accountID] = struct{}{}
}
for accountID := range targetAccountIDs {
if _, ok := credAccountIDs[accountID]; ok {
n++
}
}
}
if n >= targetN {
return nil
}
return ErrFilterNoMatch{
Err: ErrFilterSignatureUnsatisfied{
NumAccounts: n,
TargetNumAccounts: targetN,
},
}
}