package accessctl import ( "dehub/sigcred" "dehub/typeobj" "errors" "fmt" "math" "strconv" "strings" ) // ConditionInterface describes the methods that all Signifiers must implement. type ConditionInterface interface { // Satisfied asserts that the Condition is satisfied by the given set of // Credentials. If it is not (or something else went wrong) then an error is // returned. // // NOTE that Satisfied assumes the Credential has already been Verify'd. Satisfied([]sigcred.Credential) error } // Condition represents an access control condition being defined in the Config. // Only one of its fields may be filled in at a time. type Condition struct { Signature *ConditionSignature `type:"signature"` } // MarshalYAML implements the yaml.Marshaler interface. func (c Condition) MarshalYAML() (interface{}, error) { return typeobj.MarshalYAML(c) } // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *Condition) UnmarshalYAML(unmarshal func(interface{}) error) error { return typeobj.UnmarshalYAML(c, unmarshal) } // Interface returns the ConditionInterface encapsulated by this Condition // object. func (c Condition) Interface() (ConditionInterface, error) { el, _, err := typeobj.Element(c) if err != nil { return nil, err } return el.(ConditionInterface), nil } // ConditionSignature represents the configuration of an access control // condition which requires one or more signatures to be present on a commit. // // Either AccountIDs or AccountIDsByMeta must be filled. type ConditionSignature struct { AccountIDs []string `yaml:"account_ids,omitempty"` AnyAccount bool `yaml:"any_account,omitempty"` Count string `yaml:"count"` } var _ ConditionInterface = ConditionSignature{} func (condSig ConditionSignature) targetNum() (int, error) { if !strings.HasSuffix(condSig.Count, "%") { return strconv.Atoi(condSig.Count) } else if condSig.AnyAccount { return 0, errors.New("cannot use AnyAccount and a percent Count together") } percentStr := strings.TrimRight(condSig.Count, "%") percent, err := strconv.ParseFloat(percentStr, 64) if err != nil { return 0, fmt.Errorf("could not parse Count as percent %q: %w", condSig.Count, err) } targetF := float64(len(condSig.AccountIDs)) * percent / 100 targetF = math.Ceil(targetF) return int(targetF), nil } // ErrConditionSignatureUnsatisfied is returned from ConditionSignature's // Satisfied method when the Condition has not been satisfied. type ErrConditionSignatureUnsatisfied struct { TargetNumAccounts, NumAccounts int } func (err ErrConditionSignatureUnsatisfied) Error() string { return fmt.Sprintf("not enough valid signature credentials, requires %d but only had %d", err.TargetNumAccounts, err.NumAccounts) } // Satisfied asserts that the given Credentials contains enough signatures to be // satisfied. func (condSig ConditionSignature) Satisfied(creds []sigcred.Credential) error { targetN, err := condSig.targetNum() if err != nil { return fmt.Errorf("could not compute ConditionSignature target number of accounts: %w", err) } credAccountIDs := map[string]struct{}{} for _, cred := range creds { // TODO currently only signature credentials are implemented, so we can // just assume that the given AccountID has provided a sig. In the // future this may not be true. credAccountIDs[cred.AccountID] = struct{}{} } var n int if condSig.AnyAccount { // TODO this doesn't actually check that the accounts are defined in the // Config. n = len(credAccountIDs) } else { targetAccountIDs := map[string]struct{}{} for _, accountID := range condSig.AccountIDs { targetAccountIDs[accountID] = struct{}{} } for accountID := range targetAccountIDs { if _, ok := credAccountIDs[accountID]; ok { n++ } } } if n < targetN { return ErrConditionSignatureUnsatisfied{ TargetNumAccounts: targetN, NumAccounts: n, } } return nil }