1f422511d5
--- type: change message: |- completely refactor accessctl (again) This time it's using an actual access control list system, rather than whatever it was doing before. The new system uses a Filter type, rather than Condition, to decide which acl element should have its action (allow or deny) applied. This makes testing way easier, since all the different matching conditions are now individual filters, and so are tested individually. change_hash: AFgN0hormIlO0VWkLKnAdSDZeVRbh0Wj8LLXOMVQEK+L credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5yoi8ACgkQlcRvpqQRSKwo/g//QkSA80APnPtwcibNeaoUwduH1Xim8pmi5JScKGsOypYkE0Iy+fO3fRNz4r7Tm6qNn7O04fDsiXlxPojxn7+NDFCQArVgoJk3jVTRDBW7LpahWJsYPP1SBjGtaR9o0bOpclXblTMIcteTkLM94AeASqLaEY8StO+5PX/82AkFRQ8E6m9R2HCmgwbBhqwWp8936x8ekMFbSSi0TMIyV4rpd0wj4mjvjjBwa3ArGmH/ynwabPCFuuqMT6996N1zoDn5EqZA5jGrf+Q7rxsI6t1bOnLmg9NGMQYRaZLAVZrp5P6G5XR3et4Gz/2AphAEgYJM3yLbEjZW6Daa77CgTNHXde7gCaWqyfcKlVGPi29/O+2IXhpjwxHwGpsBgEdX9227zapL+jwSAOdUVj8n6C8I8BGqpT7rTwA53yxlbSwXlkttvAn/lGT5X4lK74YfkzMXMEBZKzsb/dQEPyP2Y+AG6z2D4Bs/4szsCiUXF9aG2Yx1o45lVXTTdPUNLIsnhBjM7usbQRg8i5kC+OC9AVCi8E+lf0/Qgp0cUb6QLH47bHvDTH7UluY1bgSLZy+Zjaisvl3a0aK/UspywWN/fFgOrz2cDw232n8IC+Zi4LSKm7dXDRFbC1JNzrwAPP1ifboOrltwKroOsDNaVGhX8ABahNjmrUO4JgE7gvX+zxXb+/I= account: mediocregopher
152 lines
4.3 KiB
Go
152 lines
4.3 KiB
Go
// Package accessctl implements functionality related to allowing or denying
|
|
// actions in a repo based on who is taking what actions.
|
|
package accessctl
|
|
|
|
import (
|
|
"dehub/sigcred"
|
|
"errors"
|
|
"fmt"
|
|
|
|
yaml "gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// DefaultAccessControlsStr is the encoded form of the default access control
|
|
// set which is applied to all CommitRequests if no user-supplied ones match.
|
|
//
|
|
// The effect of these AccessControls is to allow all commit types on any branch
|
|
// (with the exception of the main branch, which only allows change commits), as
|
|
// long as the commit has one signature from a configured account.
|
|
var DefaultAccessControlsStr = `
|
|
- action: allow
|
|
filters:
|
|
- type: not
|
|
filter:
|
|
type: branch
|
|
pattern: main
|
|
- type: signature
|
|
any_account: true
|
|
count: 1
|
|
|
|
- action: allow
|
|
filters:
|
|
- type: branch
|
|
pattern: main
|
|
- type: commit_type
|
|
commit_type: change
|
|
- type: signature
|
|
any_account: true
|
|
count: 1
|
|
|
|
- action: deny
|
|
`
|
|
|
|
// DefaultAccessControls is the decoded form of DefaultAccessControlsStr.
|
|
var DefaultAccessControls = func() []AccessControl {
|
|
var acl []AccessControl
|
|
if err := yaml.Unmarshal([]byte(DefaultAccessControlsStr), &acl); err != nil {
|
|
panic(err)
|
|
}
|
|
return acl
|
|
}()
|
|
|
|
// CommitRequest is used to describe a set of interactions which are being
|
|
// requested to be performed.
|
|
type CommitRequest struct {
|
|
// Type describes what type of commit is being requested. Possibilities are
|
|
// determined by the requester.
|
|
Type string
|
|
|
|
// Branch is the name of the branch the interactions are being attempted on.
|
|
// It is required.
|
|
Branch string
|
|
|
|
// Credentials are the Credential objects attached to the commit.
|
|
Credentials []sigcred.Credential
|
|
|
|
// FilesChanged is the set of file paths (relative to the repo root) which
|
|
// have been modified in some way.
|
|
FilesChanged []string
|
|
}
|
|
|
|
// Action describes what action an AccessControl should perform
|
|
// when given a CommitRequest.
|
|
type Action string
|
|
|
|
// Enumerates possible Action values
|
|
const (
|
|
ActionAllow Action = "allow"
|
|
ActionDeny Action = "deny"
|
|
|
|
// ActionNext is used internally when a request does not match an
|
|
// AccessControl's filters. It _could_ be used in the Config as well, but it
|
|
// would be pretty pointless to do so, so we don't talk about it.
|
|
ActionNext Action = "next"
|
|
)
|
|
|
|
// AccessControl describes a set of Filters, and the Actions which should be
|
|
// taken on a CommitRequest if those Filters all match on the CommitRequest.
|
|
type AccessControl struct {
|
|
Action Action `yaml:"action"`
|
|
Filters []Filter `yaml:"filters"`
|
|
}
|
|
|
|
// ActionForCommit returns what Action this AccessControl says to take for a
|
|
// given CommitRequest. It may return ActionNext if the request is not matched
|
|
// by the AccessControl's Filters.
|
|
func (ac AccessControl) ActionForCommit(req CommitRequest) (Action, error) {
|
|
for _, filter := range ac.Filters {
|
|
filterI, err := filter.Interface()
|
|
if err != nil {
|
|
return "", fmt.Errorf("casting %+v to a FilterInterface: %w", filter, err)
|
|
|
|
} else if err := filterI.MatchCommit(req); errors.As(err, new(ErrFilterNoMatch)) {
|
|
return ActionNext, nil
|
|
|
|
} else if err != nil {
|
|
// ignore the error here, if we could get the FilterInterface then
|
|
// we should be able to get the type.
|
|
filterTypeStr, _ := filter.Type()
|
|
return "", fmt.Errorf("matching commit using filter of type %q: %w", filterTypeStr, err)
|
|
}
|
|
}
|
|
return ac.Action, nil
|
|
}
|
|
|
|
// ErrCommitRequestDenied is returned from AssertCanCommit when a particular
|
|
// AccessControl has explicitly disallowed the CommitRequest.
|
|
type ErrCommitRequestDenied struct {
|
|
By AccessControl
|
|
}
|
|
|
|
func (e ErrCommitRequestDenied) Error() string {
|
|
acB, err := yaml.Marshal(e.By)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return fmt.Sprintf("commit matched and denied by this access control:\n%s", string(acB))
|
|
}
|
|
|
|
// AssertCanCommit asserts that the given CommitRequest is allowed by the given
|
|
// AccessControls.
|
|
func AssertCanCommit(acl []AccessControl, req CommitRequest) error {
|
|
acl = append(acl, DefaultAccessControls...)
|
|
for _, ac := range acl {
|
|
action, err := ac.ActionForCommit(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch action {
|
|
case ActionNext:
|
|
continue
|
|
case ActionAllow:
|
|
return nil
|
|
case ActionDeny:
|
|
return ErrCommitRequestDenied{By: ac}
|
|
default:
|
|
return fmt.Errorf("invalid action %q", action)
|
|
}
|
|
}
|
|
|
|
panic("should not be able to get here")
|
|
}
|