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 }