// Package accessctl implements functionality related to allowing or denying // actions in a repo based on who is taking what actions. package accessctl import ( "errors" "fmt" "dehub.dev/src/dehub.git/sigcred" yaml "gopkg.in/yaml.v2" ) // DefaultAccessControlsStr is the encoded form of the default access control // set which is applied to all CommitRequests if no user-supplied ones match. // // The effect of these AccessControls is to allow all commit types on any branch // (with the exception of the main branch, which only allows change commits), as // long as the commit has one signature from a configured account. var DefaultAccessControlsStr = ` - action: allow filters: - type: not filter: type: branch pattern: main - type: signature any_account: true count: 1 - action: deny filters: - type: commit_attributes non_fast_forward: true - action: allow filters: - type: branch pattern: main - type: commit_type commit_type: change - type: signature any_account: true count: 1 - action: deny ` // DefaultAccessControls is the decoded form of DefaultAccessControlsStr. var DefaultAccessControls = func() []AccessControl { var acl []AccessControl if err := yaml.Unmarshal([]byte(DefaultAccessControlsStr), &acl); err != nil { panic(err) } return acl }() // CommitRequest is used to describe a set of interactions which are being // requested to be performed. type CommitRequest struct { // Type describes what type of commit is being requested. Possibilities are // determined by the requester. Type string // Branch is the name of the branch the interactions are being attempted on. // It is required. Branch string // Credentials are the Credential objects attached to the commit. Credentials []sigcred.Credential // FilesChanged is the set of file paths (relative to the repo root) which // have been modified in some way. FilesChanged []string // NonFastForward should be set to true if the branch HEAD and this commit // are not directly related (i.e. neither is a direct ancestor of the // other). NonFastForward bool } // Action describes what action an AccessControl should perform // when given a CommitRequest. type Action string // Enumerates possible Action values const ( ActionAllow Action = "allow" ActionDeny Action = "deny" // ActionNext is used internally when a request does not match an // AccessControl's filters. It _could_ be used in the Config as well, but it // would be pretty pointless to do so, so we don't talk about it. ActionNext Action = "next" ) // AccessControl describes a set of Filters, and the Actions which should be // taken on a CommitRequest if those Filters all match on the CommitRequest. type AccessControl struct { Action Action `yaml:"action"` Filters []Filter `yaml:"filters"` } // ActionForCommit returns what Action this AccessControl says to take for a // given CommitRequest. It may return ActionNext if the request is not matched // by the AccessControl's Filters. func (ac AccessControl) ActionForCommit(req CommitRequest) (Action, error) { for _, filter := range ac.Filters { filterI, err := filter.Interface() if err != nil { return "", fmt.Errorf("casting %+v to a FilterInterface: %w", filter, err) } else if err := filterI.MatchCommit(req); errors.As(err, new(ErrFilterNoMatch)) { return ActionNext, nil } else if err != nil { // ignore the error here, if we could get the FilterInterface then // we should be able to get the type. filterTypeStr, _ := filter.Type() return "", fmt.Errorf("matching commit using filter of type %q: %w", filterTypeStr, err) } } return ac.Action, nil } // ErrCommitRequestDenied is returned from AssertCanCommit when a particular // AccessControl has explicitly disallowed the CommitRequest. type ErrCommitRequestDenied struct { By AccessControl } func (e ErrCommitRequestDenied) Error() string { acB, err := yaml.Marshal(e.By) if err != nil { panic(err) } return fmt.Sprintf("commit matched and denied by this access control:\n%s", string(acB)) } // AssertCanCommit asserts that the given CommitRequest is allowed by the given // AccessControls. func AssertCanCommit(acl []AccessControl, req CommitRequest) error { acl = append(acl, DefaultAccessControls...) for _, ac := range acl { action, err := ac.ActionForCommit(req) if err != nil { return err } switch action { case ActionNext: continue case ActionAllow: return nil case ActionDeny: return ErrCommitRequestDenied{By: ac} default: return fmt.Errorf("invalid action %q", action) } } panic("should not be able to get here") }