76309b51cb
message: |- Refactor access controls to support multiple branches This was a big lift. It implements a backwards incompatible change to overhaul access control patterns to also encompass which branch is being interacted with, not only which files. The `accessctl` package was significantly rewritten to support this, as well as some of the code modifying it. The INTRODUCTION and SPEC were also appropriately updated. The change to the SPEC is _technically_ backwards incompatible, but it won't effect anything. The `access_control` previously being used will just be ignored, and the changes to `accessctl` include the definition of fallback access controls which will automatically be applied if nothing else matches, so when verifying the older history of this repo those will be used. change_hash: AIfNYLmOLGpuyTiVodT3hDe9lF4E+5DbOTgSdkbjJONb credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5aw0sACgkQlcRvpqQRSKy7kw//UMyS/waV/tE1vntZrMbmEtFmiXPcMVNal76cjhdiF3He50qXoWG6m0qWz+arD1tbjoZml6pvU+Xt45y/Uc54DZizzz0E9azoFW0/uvZiLApftFRArZbT9GhbDs2afalyoTJx/xvQu+a2FD/zfljEWE8Zix+bwHCLojiYHHVA65HFLEt8RsH33jFyzWvS9a2yYqZXL0qrU9tdV68hazdIm1LCp+lyVV74TjwxPAZDOmNAE9l4EjIk1pgr2Qo4u2KwJqCGdVCvka8TiFFYiP7C6319ZhDMyj4m9yZsd1xGtBd9zABVBDgmzCEjt0LI3Tv35lPd2tpFBkjQy0WGcMAhwSHWSP7lxukQMCEB7og/SwtKaExiBJhf1HRO6H9MlhNSW4X1xwUgP+739ixKKUY/RcyXgZ4pkzt6sewAMVbUOcmzXdUvuyDJQ0nhDFowgicpSh1m8tTkN1aLUx18NfnGZRgkgBeE6EpT5/+NBfFwvpiQkXZ3bcMiNhNTU/UnWMyqjKlog+8Ca/7CqgswYppMaw4iPaC54H8P6JTH+XnqDlLKSkvh7PiJJa5nFDG07fqc8lYKm1KGv6virAhEsz/AYKLoNGIsqXt+mYUDLvQpjlRsiN52euxyn5e41LcrH0RidIGMVeaS+7re1pWbkCkMMMtYlnCbC5L6mfrBu6doN8o= account: mediocregopher
155 lines
4.9 KiB
Go
155 lines
4.9 KiB
Go
package accessctl
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/bmatcuk/doublestar"
|
|
)
|
|
|
|
var (
|
|
// DefaultChangeAccessControl represents the ChangeAccessControl which is
|
|
// applied when a changed file's path does not match any defined patterns
|
|
// within a BranchAccessControl.
|
|
DefaultChangeAccessControl = ChangeAccessControl{
|
|
FilePathPattern: "**",
|
|
Condition: Condition{
|
|
Signature: &ConditionSignature{
|
|
AnyAccount: true,
|
|
Count: "1",
|
|
},
|
|
},
|
|
}
|
|
|
|
// DefaultBranchAccessControls represents the BranchAccessControls which are
|
|
// applied when the name of a branch being interacted with does not match
|
|
// any defined patterns within the Config.
|
|
DefaultBranchAccessControls = []BranchAccessControl{
|
|
// These are currently the same, but they will differ once things like
|
|
// comments start being implemented.
|
|
{
|
|
BranchPattern: "trunk",
|
|
ChangeAccessControls: []ChangeAccessControl{DefaultChangeAccessControl},
|
|
},
|
|
{
|
|
BranchPattern: "**",
|
|
ChangeAccessControls: []ChangeAccessControl{DefaultChangeAccessControl},
|
|
},
|
|
}
|
|
)
|
|
|
|
// BranchAccessControl represents an access control object defined for the
|
|
// purpose of controlling who is able to perform what interactions with a
|
|
// branch.
|
|
type BranchAccessControl struct {
|
|
BranchPattern string `yaml:"branch_pattern"`
|
|
ChangeAccessControls []ChangeAccessControl `yaml:"change_access_controls"`
|
|
}
|
|
|
|
// ChangeAccessControl represents an access control object being defined in the
|
|
// Config for the purpose of controlling who is able to change which files.
|
|
type ChangeAccessControl struct {
|
|
FilePathPattern string `yaml:"file_path_pattern"`
|
|
Condition Condition `yaml:"condition"`
|
|
}
|
|
|
|
// MatchInteractions is used as an input to Match to describe all
|
|
// interactions which are being attempted on a particular Branch.
|
|
type MatchInteractions struct {
|
|
// Branch is the name of the branch the interactions are being attempted on.
|
|
// It is required.
|
|
Branch string
|
|
|
|
// FilePathsChanged is the set of file paths (relative to the repo root)
|
|
// which have been modified in some way.
|
|
FilePathsChanged []string
|
|
}
|
|
|
|
// MatchedChangeAccessControl contains information about a ChangeAccessControl
|
|
// which was matched in Match
|
|
type MatchedChangeAccessControl struct {
|
|
ChangeAccessControl ChangeAccessControl
|
|
|
|
// FilePaths contains all FilePaths to which this access control was found
|
|
// to be applicable.
|
|
FilePaths []string
|
|
}
|
|
|
|
// MatchResult is the result returned from the Match method.
|
|
type MatchResult struct {
|
|
// BranchPattern indicates the BranchPattern field of the
|
|
// BranchAccessControl object which matched the inputs.
|
|
BranchPattern string
|
|
|
|
// ChangeAccessControls indicates which ChangeAccessControl objects matched
|
|
// the files being changed.
|
|
ChangeAccessControls []MatchedChangeAccessControl
|
|
}
|
|
|
|
// Match takes in a set of access controls and a set of interactions taking
|
|
// place, and returns a MatchResult describing the access controls which should
|
|
// be applied to the interactions.
|
|
func Match(accessControls []BranchAccessControl, interactions MatchInteractions) (MatchResult, error) {
|
|
var res MatchResult
|
|
|
|
accessControls = append(accessControls, DefaultBranchAccessControls...)
|
|
|
|
// find the applicable BranchAccessControl
|
|
var branchAC BranchAccessControl
|
|
{
|
|
var ok bool
|
|
var err error
|
|
for i := range accessControls {
|
|
ok, err = doublestar.Match(accessControls[i].BranchPattern, interactions.Branch)
|
|
if err != nil {
|
|
return res, fmt.Errorf("error matching branch %q to pattern %q: %w",
|
|
accessControls[i].BranchPattern, interactions.Branch, err)
|
|
} else if ok {
|
|
branchAC = accessControls[i]
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
panic(fmt.Sprintf("no patterns matched branch %q, which shouldn't be possible", interactions.Branch))
|
|
}
|
|
res.BranchPattern = branchAC.BranchPattern
|
|
}
|
|
|
|
// determine ChangeAccessControl for each path in FilesChanged
|
|
{
|
|
changeACs := append(branchAC.ChangeAccessControls, DefaultChangeAccessControl)
|
|
acToPaths := map[ChangeAccessControl][]string{}
|
|
for _, path := range interactions.FilePathsChanged {
|
|
var ok bool
|
|
var err error
|
|
for _, ac := range changeACs {
|
|
if ok, err = doublestar.PathMatch(ac.FilePathPattern, path); err != nil {
|
|
return res, fmt.Errorf("error matching path %q to patterrn %q: %w",
|
|
path, ac.FilePathPattern, err)
|
|
} else if ok {
|
|
acToPaths[ac] = append(acToPaths[ac], path)
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
panic(fmt.Sprintf("no patterns matched change path %q, which shouldn't be possible", path))
|
|
}
|
|
}
|
|
for ac, paths := range acToPaths {
|
|
res.ChangeAccessControls = append(res.ChangeAccessControls, MatchedChangeAccessControl{
|
|
ChangeAccessControl: ac,
|
|
FilePaths: paths,
|
|
})
|
|
}
|
|
|
|
// sort result for determinancy
|
|
sort.Slice(res.ChangeAccessControls, func(i, j int) bool {
|
|
pi := res.ChangeAccessControls[i].ChangeAccessControl.FilePathPattern
|
|
pj := res.ChangeAccessControls[j].ChangeAccessControl.FilePathPattern
|
|
return pi < pj
|
|
})
|
|
}
|
|
|
|
return res, nil
|
|
}
|