dehub/accessctl/access_control.go
mediocregopher a01f2b1512 implement the ability for users without an account to still submit accredited co...
---
type: change
message: implement the ability for users without an account to still submit accredited
  commits
change_hash: AIcRB2u380KAM345cCdexq3RzhDeEuDNT9gwcGDIj8xp
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6STloACgkQlcRvpqQRSKwMvg//c0JOUTlXNpDA8VSSUdPwHPrUJix223eMOi+SzP4RlCkEh8bHT24D3P1bpMacjgNpcmHGshAwtZkboTbvHNnqVu7HvOM2Vb/lrtkod/0sD4NsO2+GjJBNJmBtv9rEEz9wBGKKhjgZc5+h3d7UlmWHiLO++v2RnjEYd13Fj4/fOHnpAWrXVodtSEFVxUGup980Ug3uvC/vc8+a9w5llafqBMAnastQ/DyulPQeMTE2lxfyGvK1EhQSxbSokO61rgNssPFyfsmheA7T1FIrOzkXqNhIFsfRB4dAOBhhtoqRFEoJ755jK4XSlE5Y8klFuRVIfdnyBlrbia+Pc8u9KMoIBk0hDP+niFqUn9lEZS5D6X7PW/8DsaHa0rlHic9IB7nE563Fm1QVd5GSj7t3/0vPBetxdmXshLuTWtq+gSEJBH2DlC7AHw6gZkSr0w4d2HJlDivfP/cWuyrp0PrOAnEuKCRpnD+EBV5+wa+QlYYIAuTLMwF+/aT/G1VCtCSFkE5JWzZVw6J2oVq25deLpe1TMQ8dHevSlPx/UcofZasO7uFHLc3xDyC8ceK+pGuvRA2SSOIGo7+qR1xh2EhmQ2RZO1AN0NB4NYHQixYqWERen8SDe1jsSy6ercKTE5T/jJeHVPIOm1nutdP+D5gjQGU0JzcNoG/luJv02MWoD7J7RWU=
  account: mediocregopher
2020-04-11 17:10:18 -06:00

153 lines
4.4 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 (
"errors"
"fmt"
"dehub.dev/src/dehub.git/sigcred"
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")
}