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 _ Filter = 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, }, } }