You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.5 KiB
202 lines
6.5 KiB
package accessctl
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/bmatcuk/doublestar"
|
|
)
|
|
|
|
var (
|
|
// DefaultSignatureCondition represents the Condition which is applied for
|
|
// default access controls. It requires a single signature credential from
|
|
// any account defined in the Config.
|
|
DefaultSignatureCondition = Condition{
|
|
Signature: &ConditionSignature{
|
|
AnyAccount: true,
|
|
Count: "1",
|
|
},
|
|
}
|
|
|
|
// 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: DefaultSignatureCondition,
|
|
}
|
|
|
|
// DefaultCredentialAccessControl represents the CredentialAccessControl
|
|
// which is applied when a BranchAccessControl does not have a defined
|
|
// CredentialAccessControl.
|
|
DefaultCredentialAccessControl = CredentialAccessControl{
|
|
Condition: DefaultSignatureCondition,
|
|
}
|
|
|
|
// 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{
|
|
{
|
|
BranchPattern: "main",
|
|
CredentialAccessControl: &CredentialAccessControl{
|
|
Condition: Condition{Never: new(ConditionNever)},
|
|
},
|
|
},
|
|
{
|
|
BranchPattern: "**",
|
|
},
|
|
}
|
|
)
|
|
|
|
// any account can do anything, except main branch can only get change commits
|
|
|
|
// 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,omitempty"`
|
|
CredentialAccessControl *CredentialAccessControl `yaml:"credential_access_control,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// CredentialAccessControl represents an access control object being defined in
|
|
// the Config for the purpose of controlling who is able to create credential
|
|
// commits.
|
|
type CredentialAccessControl struct {
|
|
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
|
|
|
|
// CredentialAdded indicates a credential commit is being added to the
|
|
// Branch.
|
|
CredentialAdded bool
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// MatchedCredentialAccessControl contains information about a
|
|
// CredentialAccessControl which was matched in Match.
|
|
type MatchedCredentialAccessControl struct {
|
|
CredentialAccessControl CredentialAccessControl
|
|
}
|
|
|
|
// 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
|
|
|
|
// CredentialAccessControls indicates which CredentialAccessControl object
|
|
// matched for a credential commit. Will be nil if CredentialAdded was
|
|
// false.
|
|
CredentialAccessControl *MatchedCredentialAccessControl
|
|
}
|
|
|
|
// 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("matching branch %q to branch_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("matching path %q to file_path_pattern %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
|
|
})
|
|
}
|
|
|
|
// Handle CredentialAccessControl, if applicable
|
|
if interactions.CredentialAdded {
|
|
credAC := branchAC.CredentialAccessControl
|
|
if credAC == nil {
|
|
credAC = &DefaultCredentialAccessControl
|
|
}
|
|
|
|
res.CredentialAccessControl = &MatchedCredentialAccessControl{
|
|
CredentialAccessControl: *credAC,
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|