dehub/accessctl/access_control.go
mediocregopher 326de2afc6 Fully implement credential commits
---
type: change
message: |-
  Fully implement credential commits

  The actual commit objects and related refactoring had already been done, this
  commit takes the next step of implementing the access control changes, tests for
  verification, and refactoring of the dehub command to support multiple commit
  message types (as well as a small fix to dcmd).
change_hash: AJyuAR0koVoe+uPBisa5qXsbW8YhlgOKNhnvy9uv7hQ8
credentials:
- type: pgp_signature
  pub_key_id: 95C46FA6A41148AC
  body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5tVzoACgkQlcRvpqQRSKyznw//b9lWd4V4G81cFwGAxZtJ3JiFpspYdtTAUUcLi9nogGsDmqkkSQxLdmBCT99QtaenKsxpad+9sXhkZpgWF/AyCX9pN6TTlMKuRcDXeoMUjeKjRpRhCHN0Lt8Sz80NDPYIa81r9cH0o1987GirgGmDEkYNDAFPDdGNDcCad/LLnG+ONwOl9WEM1q5O4etUPurTywlBiELDjHxeLzqgxCo8fMaMejW6mxnMDV6DIHiX6INWZAAG66HPVetmq6EVl9bnFgZmgKNzqKzFVZJRdGQNbhR/WzlOh0HPyJGwCveZPM5Zjd/dpfQUYEGGprVKc0G0YVNU2Hcz6O7hqafGGxWpCFW6zKrNmBRaW2u2zjVJD4ukmWn9gFuKJKhs0kyawRTbHNIX+gonYv9lDFO3cZ5qcsJbSAYSHrCav121z0GsQDoFJMJDQnP0syEEbAaxdQe7Bd7bmOM3SpCOLJLF1+X7Srrq5//u6fiFDxQ82Ylo3hG/r7/QT/vSipUCglx4POq33+z8VEHGhVfl4dgSU6OgIV/S7evKC7EiS/jh/xywU44RHpxFhwS3hthHxZqgRIHTm65DqGYWWZds2Hkr29TTRajuf0t4MxqY2MrLAhNJUc6OmrVN+lWMmm/z1FEhfrOvZ8v7mOSqTKwkvbsZzk5mpeo2RrLdNnnWvTCy87FpA48=
  account: mediocregopher
2020-03-14 16:14:18 -06:00

203 lines
6.5 KiB
Go

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
}