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: "main", 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 }