Refactor access controls to support multiple branches
message: |- Refactor access controls to support multiple branches This was a big lift. It implements a backwards incompatible change to overhaul access control patterns to also encompass which branch is being interacted with, not only which files. The `accessctl` package was significantly rewritten to support this, as well as some of the code modifying it. The INTRODUCTION and SPEC were also appropriately updated. The change to the SPEC is _technically_ backwards incompatible, but it won't effect anything. The `access_control` previously being used will just be ignored, and the changes to `accessctl` include the definition of fallback access controls which will automatically be applied if nothing else matches, so when verifying the older history of this repo those will be used. change_hash: AIfNYLmOLGpuyTiVodT3hDe9lF4E+5DbOTgSdkbjJONb credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5aw0sACgkQlcRvpqQRSKy7kw//UMyS/waV/tE1vntZrMbmEtFmiXPcMVNal76cjhdiF3He50qXoWG6m0qWz+arD1tbjoZml6pvU+Xt45y/Uc54DZizzz0E9azoFW0/uvZiLApftFRArZbT9GhbDs2afalyoTJx/xvQu+a2FD/zfljEWE8Zix+bwHCLojiYHHVA65HFLEt8RsH33jFyzWvS9a2yYqZXL0qrU9tdV68hazdIm1LCp+lyVV74TjwxPAZDOmNAE9l4EjIk1pgr2Qo4u2KwJqCGdVCvka8TiFFYiP7C6319ZhDMyj4m9yZsd1xGtBd9zABVBDgmzCEjt0LI3Tv35lPd2tpFBkjQy0WGcMAhwSHWSP7lxukQMCEB7og/SwtKaExiBJhf1HRO6H9MlhNSW4X1xwUgP+739ixKKUY/RcyXgZ4pkzt6sewAMVbUOcmzXdUvuyDJQ0nhDFowgicpSh1m8tTkN1aLUx18NfnGZRgkgBeE6EpT5/+NBfFwvpiQkXZ3bcMiNhNTU/UnWMyqjKlog+8Ca/7CqgswYppMaw4iPaC54H8P6JTH+XnqDlLKSkvh7PiJJa5nFDG07fqc8lYKm1KGv6virAhEsz/AYKLoNGIsqXt+mYUDLvQpjlRsiN52euxyn5e41LcrH0RidIGMVeaS+7re1pWbkCkMMMtYlnCbC5L6mfrBu6doN8o= account: mediocregopher
This commit is contained in:
parent
2add3a2501
commit
76309b51cb
@ -4,11 +4,3 @@ accounts:
|
||||
signifiers:
|
||||
- type: pgp_public_key_file
|
||||
path: ".dehub/mediocregopher.asc"
|
||||
|
||||
access_controls:
|
||||
- pattern: "**"
|
||||
condition:
|
||||
type: signature
|
||||
account_ids:
|
||||
- mediocregopher
|
||||
count: 100%
|
||||
|
@ -39,9 +39,9 @@ platforms into the git history itself, including dehub's own configuration.
|
||||
|
||||
By doing this, the server-side git component can be reduced to a mere
|
||||
pre-receive hook (if anything at all). This opens the door for much more
|
||||
lightweight and flexible hosting of git projects, and even more radical
|
||||
solutions like hosting git projects on completely decentralized platforms like
|
||||
IPFS.
|
||||
lightweight and flexible hosting of git projects, as well as even more radical
|
||||
solutions; dehub can enable hosting git projects on completely decentralized
|
||||
platforms like IPFS.
|
||||
|
||||
### Example
|
||||
|
||||
@ -53,16 +53,17 @@ equivalent of the `master` branch). MyProject's repo would contain a
|
||||
```
|
||||
# ...
|
||||
access_controls:
|
||||
|
||||
# matches all files, but could be used for more fine-grained control
|
||||
- pattern: "**"
|
||||
condition:
|
||||
type: signature
|
||||
account_ids:
|
||||
- alice
|
||||
- bob
|
||||
- carol
|
||||
count: 2
|
||||
- branch_pattern: trunk
|
||||
change_access_controls:
|
||||
# matches all files, but could be used for more fine-grained control
|
||||
- file_path_pattern: "**"
|
||||
condition:
|
||||
type: signature
|
||||
account_ids:
|
||||
- alice
|
||||
- bob
|
||||
- carol
|
||||
count: 2
|
||||
```
|
||||
|
||||
A commit in the `trunk` branch would have a message with the following form:
|
||||
@ -102,8 +103,7 @@ needed to verify commits in `trunk` when they are pushed or pulled.
|
||||
|
||||
## dehub Thread Branches
|
||||
|
||||
The `trunk` branch is the project's source-of-truth; all commits in it must have
|
||||
dehub encoded message bodies with acceptable credentials. Other branches, called
|
||||
The `trunk` branch is the project's source-of-truth. Other branches, called
|
||||
threads, are used to coordinate new changes, and then coalesce those changes
|
||||
into a commit suitable for `trunk`.
|
||||
|
||||
|
77
SPEC.md
77
SPEC.md
@ -28,41 +28,60 @@ accounts:
|
||||
- type: "keybase"
|
||||
user: "some_keybase_user_id"
|
||||
|
||||
# access_controls defines under what conditions different files in the repo may
|
||||
# be modified. For each file modified in a commit, all access control patterns
|
||||
# are applied sequentially until one matches, and the associated access control
|
||||
# conditions are checked. A commit is only allowed if the conditions of all
|
||||
# modified files are met.
|
||||
# access_controls define who may do what in the repo. The value is a list of
|
||||
# access control objects, each applying to one or more potential branch names.
|
||||
access_controls:
|
||||
|
||||
# pattern is a glob pattern describing what files this access control
|
||||
# applies to. Single star matches all characters except path separators,
|
||||
# double star matches everything.
|
||||
- pattern: ".dehub/**"
|
||||
# branch_pattern is a glob pattern describing what branch names this access
|
||||
# control applies to. The first matching branch_pattern for a branch name
|
||||
# defines which access controls are applied.
|
||||
- branch_pattern: trunk
|
||||
|
||||
# signature conditions indicate that a commit must be signed by one or
|
||||
# more accounts to be allowed.
|
||||
condition:
|
||||
type: signature
|
||||
# change_access_controls is an array of possible access controls applied for
|
||||
# files being changed in the branch
|
||||
change_access_controls:
|
||||
|
||||
# account_ids lists all accounts whose signature will count towards
|
||||
# meeting the condition
|
||||
account_ids:
|
||||
- some_user_id
|
||||
# file_path_pattern is a glob pattern describing what files this access control
|
||||
# applies to. Single star matches all characters except path separators,
|
||||
# double star matches everything. The first matching file_path_pattern for a
|
||||
# file path (relative to the repo root) defines which access controls are
|
||||
# applied.
|
||||
- file_path_pattern: ".dehub/**"
|
||||
|
||||
# count describes how many signatures are required. It can be either a
|
||||
# contrete integer (e.g. 2, meaning any 2 accounts listed by
|
||||
# account_ids) or a percent.
|
||||
count: 100%
|
||||
# signature conditions indicate that a commit must be signed by one or
|
||||
# more accounts in order to be allowed.
|
||||
condition:
|
||||
type: signature
|
||||
|
||||
# This catch-all pattern for the rest of the repo requires that changes to
|
||||
# any files not under `.dehub/` are signed by at least one of the
|
||||
# defined accounts.
|
||||
- pattern: "**"
|
||||
condition:
|
||||
type: signature
|
||||
any_account: true # indicates any account defined in accounts is valid
|
||||
count: 1
|
||||
# account_ids lists all accounts whose signature will count towards
|
||||
# meeting the condition
|
||||
account_ids:
|
||||
- some_user_id
|
||||
|
||||
# count describes how many signatures are required. It can be either a
|
||||
# contrete integer (e.g. 2, meaning any 2 accounts listed by
|
||||
# account_ids) or a percent.
|
||||
count: 100% # all accounts in account_ids must sign
|
||||
|
||||
# This catch-all pattern for the rest of the repo requires that changes to
|
||||
# any files not under `.dehub/` are signed by at least one of the
|
||||
# defined accounts.
|
||||
- file_path_pattern: "**"
|
||||
condition:
|
||||
type: signature
|
||||
any_account: true # indicates any account defined in accounts is valid
|
||||
count: 1
|
||||
|
||||
# If a branch is not matched by any access control object then the following
|
||||
# default object is implied:
|
||||
#
|
||||
# branch_pattern: **
|
||||
# change_access_controls:
|
||||
# - file_path_pattern: **
|
||||
# condition:
|
||||
# type: signature
|
||||
# any_account: true
|
||||
# count: 1
|
||||
```
|
||||
|
||||
# Change Hash
|
||||
|
@ -2,52 +2,153 @@ package accessctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/bmatcuk/doublestar"
|
||||
)
|
||||
|
||||
// AccessControl represents an access control object being defined in the
|
||||
// Config.
|
||||
type AccessControl struct {
|
||||
Pattern string `yaml:"pattern"`
|
||||
Condition Condition `yaml:"condition"`
|
||||
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: "trunk",
|
||||
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"`
|
||||
}
|
||||
|
||||
// ErrNoApplicableAccessControls is returned from ApplicableAccessControls when
|
||||
// a changed path has no applicable AccessControls which match it.
|
||||
type ErrNoApplicableAccessControls struct {
|
||||
Path string
|
||||
// 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"`
|
||||
}
|
||||
|
||||
func (err ErrNoApplicableAccessControls) Error() string {
|
||||
return fmt.Sprintf("no AccessControls which apply to changed file %q", err.Path)
|
||||
// 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
|
||||
}
|
||||
|
||||
// ApplicableAccessControls returns a subset of the given AccessControls which
|
||||
// are applicable to the given file paths (ie those whose Conditions must be met
|
||||
// in order for the changes to go through.
|
||||
func ApplicableAccessControls(accessControls []AccessControl, filesChanged []string) ([]AccessControl, error) {
|
||||
applicableSet := map[AccessControl]struct{}{}
|
||||
for _, path := range filesChanged {
|
||||
var any bool
|
||||
for _, ac := range accessControls {
|
||||
if ok, err := doublestar.PathMatch(ac.Pattern, path); err != nil {
|
||||
return nil, fmt.Errorf("error matching path %q to patterrn %q: %w",
|
||||
path, ac.Pattern, err)
|
||||
// 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 {
|
||||
applicableSet[ac] = struct{}{}
|
||||
any = true
|
||||
branchAC = accessControls[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return nil, ErrNoApplicableAccessControls{Path: path}
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("no patterns matched branch %q, which shouldn't be possible", interactions.Branch))
|
||||
}
|
||||
res.BranchPattern = branchAC.BranchPattern
|
||||
}
|
||||
|
||||
applicable := make([]AccessControl, 0, len(applicableSet))
|
||||
for ac := range applicableSet {
|
||||
applicable = append(applicable, ac)
|
||||
// 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 applicable, nil
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
@ -1,117 +1,177 @@
|
||||
package accessctl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
func TestApplicableAccessControls(t *testing.T) {
|
||||
func normalizeResult(res MatchResult) MatchResult {
|
||||
if len(res.ChangeAccessControls) == 0 {
|
||||
res.ChangeAccessControls = nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
secondCond := Condition{
|
||||
Signature: &ConditionSignature{
|
||||
AnyAccount: true,
|
||||
Count: "2",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
descr string
|
||||
patterns, filesChanged []string
|
||||
exp []string
|
||||
expErrPath string
|
||||
descr string
|
||||
|
||||
branchACs []BranchAccessControl
|
||||
interactions MatchInteractions
|
||||
result MatchResult
|
||||
}{
|
||||
{
|
||||
descr: "empty input empty output",
|
||||
descr: "empty input empty result",
|
||||
result: MatchResult{
|
||||
BranchPattern: "**",
|
||||
},
|
||||
},
|
||||
{
|
||||
descr: "empty patterns",
|
||||
filesChanged: []string{"foo", "bar"},
|
||||
expErrPath: "foo",
|
||||
descr: "empty access controls",
|
||||
interactions: MatchInteractions{
|
||||
Branch: "trunk",
|
||||
FilePathsChanged: []string{"foo", "bar"},
|
||||
},
|
||||
result: MatchResult{
|
||||
BranchPattern: "trunk",
|
||||
ChangeAccessControls: []MatchedChangeAccessControl{
|
||||
{
|
||||
ChangeAccessControl: DefaultChangeAccessControl,
|
||||
FilePaths: []string{"foo", "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
descr: "empty filesChanged",
|
||||
patterns: []string{"patternA", "patternB"},
|
||||
descr: "empty filesPathsChanged",
|
||||
branchACs: DefaultBranchAccessControls,
|
||||
interactions: MatchInteractions{Branch: "trunk"},
|
||||
result: MatchResult{BranchPattern: "trunk"},
|
||||
},
|
||||
{
|
||||
descr: "no applicable files",
|
||||
filesChanged: []string{"foo"},
|
||||
patterns: []string{"bar"},
|
||||
expErrPath: "foo",
|
||||
descr: "no matching branch patterns",
|
||||
branchACs: []BranchAccessControl{{
|
||||
BranchPattern: "dunk",
|
||||
ChangeAccessControls: []ChangeAccessControl{{
|
||||
FilePathPattern: "**",
|
||||
Condition: secondCond,
|
||||
}},
|
||||
}},
|
||||
interactions: MatchInteractions{
|
||||
Branch: "crunk",
|
||||
FilePathsChanged: []string{"foo"},
|
||||
},
|
||||
result: MatchResult{
|
||||
BranchPattern: "**",
|
||||
ChangeAccessControls: []MatchedChangeAccessControl{{
|
||||
ChangeAccessControl: DefaultChangeAccessControl,
|
||||
FilePaths: []string{"foo"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
descr: "all applicable files",
|
||||
filesChanged: []string{"foo", "bar"},
|
||||
patterns: []string{"**"},
|
||||
exp: []string{"**"},
|
||||
descr: "no matching files",
|
||||
branchACs: []BranchAccessControl{{
|
||||
BranchPattern: "trunk",
|
||||
ChangeAccessControls: []ChangeAccessControl{{
|
||||
FilePathPattern: "boo",
|
||||
Condition: secondCond,
|
||||
}},
|
||||
}},
|
||||
interactions: MatchInteractions{
|
||||
Branch: "trunk",
|
||||
FilePathsChanged: []string{"foo"},
|
||||
},
|
||||
result: MatchResult{
|
||||
BranchPattern: "trunk",
|
||||
ChangeAccessControls: []MatchedChangeAccessControl{{
|
||||
ChangeAccessControl: DefaultChangeAccessControl,
|
||||
FilePaths: []string{"foo"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
descr: "pattern precedent",
|
||||
filesChanged: []string{"foo"},
|
||||
patterns: []string{"foo", "**"},
|
||||
exp: []string{"foo"},
|
||||
descr: "branch pattern precedent",
|
||||
branchACs: []BranchAccessControl{
|
||||
{
|
||||
BranchPattern: "trunk",
|
||||
ChangeAccessControls: []ChangeAccessControl{{
|
||||
FilePathPattern: "foo",
|
||||
Condition: secondCond,
|
||||
}},
|
||||
},
|
||||
{
|
||||
BranchPattern: "**",
|
||||
ChangeAccessControls: []ChangeAccessControl{
|
||||
DefaultChangeAccessControl,
|
||||
},
|
||||
},
|
||||
},
|
||||
interactions: MatchInteractions{
|
||||
Branch: "trunk",
|
||||
FilePathsChanged: []string{"foo"},
|
||||
},
|
||||
result: MatchResult{
|
||||
BranchPattern: "trunk",
|
||||
ChangeAccessControls: []MatchedChangeAccessControl{{
|
||||
ChangeAccessControl: ChangeAccessControl{
|
||||
FilePathPattern: "foo",
|
||||
Condition: secondCond,
|
||||
},
|
||||
FilePaths: []string{"foo"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
descr: "pattern precedent inv",
|
||||
filesChanged: []string{"foo"},
|
||||
patterns: []string{"**", "foo"},
|
||||
exp: []string{"**"},
|
||||
},
|
||||
{
|
||||
descr: "individual matches",
|
||||
filesChanged: []string{"foo", "bar/baz"},
|
||||
patterns: []string{"foo", "bar/baz"},
|
||||
exp: []string{"foo", "bar/baz"},
|
||||
},
|
||||
{
|
||||
descr: "star match dir",
|
||||
filesChanged: []string{"foo", "bar/baz"},
|
||||
patterns: []string{"foo", "bar/*"},
|
||||
exp: []string{"foo", "bar/*"},
|
||||
},
|
||||
{
|
||||
descr: "star not match dir",
|
||||
filesChanged: []string{"foo", "bar/baz/biz"},
|
||||
patterns: []string{"foo", "bar/*"},
|
||||
expErrPath: "bar/baz/biz",
|
||||
},
|
||||
{
|
||||
descr: "doublestar match dir",
|
||||
filesChanged: []string{"foo", "bar/bar", "bar/baz/biz"},
|
||||
patterns: []string{"foo", "bar/**"},
|
||||
exp: []string{"foo", "bar/**"},
|
||||
descr: "multiple files matching FilePathPatterns",
|
||||
branchACs: []BranchAccessControl{{
|
||||
BranchPattern: "trunk",
|
||||
ChangeAccessControls: []ChangeAccessControl{{
|
||||
FilePathPattern: "foo*",
|
||||
Condition: secondCond,
|
||||
}},
|
||||
}},
|
||||
interactions: MatchInteractions{
|
||||
Branch: "trunk",
|
||||
FilePathsChanged: []string{"foo_a", "bar", "foo_b"},
|
||||
},
|
||||
result: MatchResult{
|
||||
BranchPattern: "trunk",
|
||||
ChangeAccessControls: []MatchedChangeAccessControl{
|
||||
{
|
||||
ChangeAccessControl: DefaultChangeAccessControl,
|
||||
FilePaths: []string{"bar"},
|
||||
},
|
||||
{
|
||||
ChangeAccessControl: ChangeAccessControl{
|
||||
FilePathPattern: "foo*",
|
||||
Condition: secondCond,
|
||||
},
|
||||
FilePaths: []string{"foo_a", "foo_b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.descr, func(t *testing.T) {
|
||||
accessControls := make([]AccessControl, len(test.patterns))
|
||||
for i := range test.patterns {
|
||||
accessControls[i] = AccessControl{Pattern: test.patterns[i]}
|
||||
res, err := Match(test.branchACs, test.interactions)
|
||||
if err != nil {
|
||||
t.Fatalf("error matching: %v", err)
|
||||
}
|
||||
|
||||
out, err := ApplicableAccessControls(accessControls, test.filesChanged)
|
||||
if err != nil && test.expErrPath == "" {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
} else if test.expErrPath != "" {
|
||||
if noAppErr := (ErrNoApplicableAccessControls{}); !errors.As(err, &noAppErr) {
|
||||
t.Fatalf("expected ErrNoApplicableAccessControls for path %q, but got %v", test.expErrPath, err)
|
||||
} else if test.expErrPath != noAppErr.Path {
|
||||
t.Fatalf("expected ErrNoApplicableAccessControls for path %q, but got one for path %q", test.expErrPath, noAppErr.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
outPatterns := make([]string, len(out))
|
||||
for i := range out {
|
||||
outPatterns[i] = out[i].Pattern
|
||||
}
|
||||
|
||||
clean := func(s []string) []string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(s)
|
||||
return s
|
||||
}
|
||||
|
||||
outPatterns = clean(outPatterns)
|
||||
test.exp = clean(test.exp)
|
||||
if !reflect.DeepEqual(outPatterns, test.exp) {
|
||||
t.Fatalf("expected: %+v\ngot: %+v", test.exp, outPatterns)
|
||||
res, expRes := normalizeResult(res), normalizeResult(test.result)
|
||||
if !reflect.DeepEqual(res, expRes) {
|
||||
t.Fatalf("expected:%s\ngot: %s", spew.Sdump(expRes), spew.Sdump(res))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -174,6 +174,7 @@ var subCmds = []subCmd{
|
||||
descr: "verifies one or more commits as having the proper credentials",
|
||||
body: func(sctx subCmdCtx) error {
|
||||
rev := sctx.flag.String("rev", "HEAD", "Revision of commit to verify")
|
||||
branch := sctx.flag.String("branch", "", "Branch that the revision is on. If not given then the currently checked out branch is assumed")
|
||||
sctx.flagParse()
|
||||
|
||||
h, err := sctx.repo().GitRepo.ResolveRevision(plumbing.Revision(*rev))
|
||||
@ -181,7 +182,16 @@ var subCmds = []subCmd{
|
||||
return fmt.Errorf("could not resolve revision %q: %w", *rev, err)
|
||||
}
|
||||
|
||||
if err := sctx.repo().VerifyChangeCommit(*h); err != nil {
|
||||
var branchName plumbing.ReferenceName
|
||||
if *branch == "" {
|
||||
if branchName, err = sctx.repo().CheckedOutBranch(); err != nil {
|
||||
return fmt.Errorf("could not determined currently checked out branch: %w", err)
|
||||
}
|
||||
} else {
|
||||
branchName = plumbing.NewBranchReferenceName(*branch)
|
||||
}
|
||||
|
||||
if err := sctx.repo().VerifyChangeCommit(branchName, *h); err != nil {
|
||||
return fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err)
|
||||
}
|
||||
|
||||
@ -209,15 +219,14 @@ var subCmds = []subCmd{
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error reading next line from stdin: %w", err)
|
||||
}
|
||||
fmt.Printf("Processing line %q\n", strings.TrimSpace(line))
|
||||
|
||||
lineParts := strings.Fields(line)
|
||||
if len(lineParts) < 3 {
|
||||
return fmt.Errorf("malformed pre-receive hook stdin line %q", line)
|
||||
}
|
||||
|
||||
if plumbing.ReferenceName(lineParts[2]) != dehub.Trunk {
|
||||
return fmt.Errorf("only commits to the trunk branch are allowed at the moment (tried to push to %q)", lineParts[2])
|
||||
}
|
||||
branchName := plumbing.ReferenceName(lineParts[2])
|
||||
|
||||
// the zeroRevision gets sent on the very first push
|
||||
const zeroRevision plumbing.Revision = "0000000000000000000000000000000000000000"
|
||||
@ -269,8 +278,8 @@ var subCmds = []subCmd{
|
||||
for i := len(hashesToCheck) - 1; i >= 0; i-- {
|
||||
hash := hashesToCheck[i]
|
||||
fmt.Printf("Verifying change commit %q\n", hash)
|
||||
if err := sctx.repo().VerifyChangeCommit(hash); err != nil {
|
||||
return fmt.Errorf("could not verify change commit %q", hash)
|
||||
if err := sctx.repo().VerifyChangeCommit(branchName, hash); err != nil {
|
||||
return fmt.Errorf("could not verify change commit %q: %w", hash, err)
|
||||
}
|
||||
}
|
||||
fmt.Println("All pushed commits have been verified, well done.")
|
||||
|
80
commit.go
80
commit.go
@ -116,8 +116,8 @@ func (r *Repo) HasStagedChanges() (bool, error) {
|
||||
return any, nil
|
||||
}
|
||||
|
||||
// NewChangeCommit constructs a ChangeCommit using the given SignifierInterface
|
||||
// to create a Credential for it.
|
||||
// NewChangeCommit constructs a ChangeCommit. If sig is given then it is used to
|
||||
// create a Credential for the ChangeCommit.
|
||||
func (r *Repo) NewChangeCommit(msg, accountID string, sig sigcred.SignifierInterface) (ChangeCommit, error) {
|
||||
_, headTree, err := r.head()
|
||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
||||
@ -131,49 +131,38 @@ func (r *Repo) NewChangeCommit(msg, accountID string, sig sigcred.SignifierInter
|
||||
return ChangeCommit{}, err
|
||||
}
|
||||
|
||||
// this is necessarily different than headTree for the case of there being
|
||||
// no HEAD (ie it's the first commit). In that case we want headTree to be
|
||||
// empty (because it's being used to generate the change hash), but we want
|
||||
// the signifier to use the raw fs (because that's where the signifier's
|
||||
// data might be).
|
||||
sigFS, err := r.headOrRawFS()
|
||||
if err != nil {
|
||||
return ChangeCommit{}, err
|
||||
}
|
||||
|
||||
cfg, err := r.loadConfig(sigFS)
|
||||
if err != nil {
|
||||
return ChangeCommit{}, fmt.Errorf("could not load config: %w", err)
|
||||
}
|
||||
|
||||
changeHash := genChangeHash(nil, msg, headTree, stagedTree)
|
||||
cred, err := sig.Sign(sigFS, changeHash)
|
||||
if err != nil {
|
||||
return ChangeCommit{}, fmt.Errorf("failed to sign commit hash: %w", err)
|
||||
}
|
||||
cred.AccountID = accountID
|
||||
|
||||
// This isn't strictly necessary, but we want to save people the effort of
|
||||
// creating an invalid commit, pushing it, having it be rejected, then
|
||||
// having to reset on the commit.
|
||||
err = r.assertAccessControls(
|
||||
cfg.AccessControls, []sigcred.Credential{cred},
|
||||
headTree, stagedTree,
|
||||
)
|
||||
if err != nil {
|
||||
return ChangeCommit{}, fmt.Errorf("commit would not satisfy access controls: %w", err)
|
||||
var creds []sigcred.Credential
|
||||
if sig != nil {
|
||||
// this is necessarily different than headTree for the case of there
|
||||
// being no HEAD (ie it's the first commit). In that case we want
|
||||
// headTree to be empty (because it's being used to generate the change
|
||||
// hash), but we want the signifier to use the raw fs (because that's
|
||||
// where the signifier's data might be).
|
||||
sigFS, err := r.headOrRawFS()
|
||||
if err != nil {
|
||||
return ChangeCommit{}, err
|
||||
}
|
||||
|
||||
cred, err := sig.Sign(sigFS, changeHash)
|
||||
if err != nil {
|
||||
return ChangeCommit{}, fmt.Errorf("failed to sign commit hash: %w", err)
|
||||
}
|
||||
cred.AccountID = accountID
|
||||
creds = append(creds, cred)
|
||||
}
|
||||
|
||||
return ChangeCommit{
|
||||
Message: msg,
|
||||
ChangeHash: changeHash,
|
||||
Credentials: []sigcred.Credential{cred},
|
||||
Credentials: creds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repo) assertAccessControls(
|
||||
accessCtls []accessctl.AccessControl, creds []sigcred.Credential,
|
||||
from, to *object.Tree,
|
||||
accessCtls []accessctl.BranchAccessControl, creds []sigcred.Credential,
|
||||
branch plumbing.ReferenceName, from, to *object.Tree,
|
||||
) error {
|
||||
filesChanged, err := calcDiff(from, to)
|
||||
if err != nil {
|
||||
@ -185,18 +174,23 @@ func (r *Repo) assertAccessControls(
|
||||
pathsChanged[i] = filesChanged[i].path
|
||||
}
|
||||
|
||||
accessCtls, err = accessctl.ApplicableAccessControls(accessCtls, pathsChanged)
|
||||
matchRes, err := accessctl.Match(accessCtls, accessctl.MatchInteractions{
|
||||
Branch: branch.Short(),
|
||||
FilePathsChanged: pathsChanged,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine applicable access controls: %w", err)
|
||||
}
|
||||
|
||||
for _, accessCtl := range accessCtls {
|
||||
condInt, err := accessCtl.Condition.Interface()
|
||||
for _, matchedAC := range matchRes.ChangeAccessControls {
|
||||
ac := matchedAC.ChangeAccessControl
|
||||
condInt, err := ac.Condition.Interface()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not cast Condition to interface: %w", err)
|
||||
return fmt.Errorf("could not cast Condition of file path pattern %q to interface: %w",
|
||||
ac.FilePathPattern, err)
|
||||
} else if err := condInt.Satisfied(creds); err != nil {
|
||||
return fmt.Errorf("access control for pattern %q not satisfied: %w",
|
||||
accessCtl.Pattern, err)
|
||||
return fmt.Errorf("access control of file path pattern %q not satisfied: %w\nFiles matched:\n%s",
|
||||
ac.FilePathPattern, err, strings.Join(matchedAC.FilePaths, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,8 +198,8 @@ func (r *Repo) assertAccessControls(
|
||||
}
|
||||
|
||||
// VerifyChangeCommit verifies that the change commit at the given hash, which
|
||||
// is presumably on the repo trunk, is gucci.
|
||||
func (r *Repo) VerifyChangeCommit(h plumbing.Hash) error {
|
||||
// is presumably on the given branch, is gucci.
|
||||
func (r *Repo) VerifyChangeCommit(branch plumbing.ReferenceName, h plumbing.Hash) error {
|
||||
commit, err := r.GitRepo.CommitObject(h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve commit object: %w", err)
|
||||
@ -241,7 +235,7 @@ func (r *Repo) VerifyChangeCommit(h plumbing.Hash) error {
|
||||
|
||||
err = r.assertAccessControls(
|
||||
cfg.AccessControls, changeCommit.Credentials,
|
||||
parentTree, commitTree,
|
||||
branch, parentTree, commitTree,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to satisfy all access controls: %w", err)
|
||||
|
@ -1,14 +1,13 @@
|
||||
package dehub
|
||||
|
||||
import (
|
||||
"dehub/accessctl"
|
||||
"dehub/sigcred"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
@ -82,7 +81,7 @@ func TestChangeCommitVerify(t *testing.T) {
|
||||
account := h.cfg.Accounts[0]
|
||||
|
||||
changeCommit, hash := h.changeCommit(step.msg, account.ID, h.sig)
|
||||
if err := h.repo.VerifyChangeCommit(hash); err != nil {
|
||||
if err := h.repo.VerifyChangeCommit(TrunkRefName, hash); err != nil {
|
||||
t.Fatalf("could not verify hash %v: %v", hash, err)
|
||||
}
|
||||
|
||||
@ -119,8 +118,8 @@ func TestConfigChange(t *testing.T) {
|
||||
_, hash := h.changeCommit("commit configuration", h.cfg.Accounts[0].ID, h.sig)
|
||||
hashes = append(hashes, hash)
|
||||
|
||||
// create a new account and add it to the configuration. It should not be
|
||||
// able to actually make that commit though.
|
||||
// create a new account and add it to the configuration. That commit should
|
||||
// not be verifiable, though
|
||||
newSig, newPubKeyBody := sigcred.SignifierPGPTmp(h.rand)
|
||||
h.cfg.Accounts = append(h.cfg.Accounts, Account{
|
||||
ID: "toot",
|
||||
@ -128,21 +127,23 @@ func TestConfigChange(t *testing.T) {
|
||||
Body: string(newPubKeyBody),
|
||||
}}},
|
||||
})
|
||||
h.cfg.AccessControls[0].Condition.Signature.AccountIDs = []string{"root", "toot"}
|
||||
h.cfg.AccessControls[0].Condition.Signature.Count = "1"
|
||||
h.cfg.AccessControls[0].ChangeAccessControls[0].Condition.Signature.AccountIDs = []string{"root", "toot"}
|
||||
h.cfg.AccessControls[0].ChangeAccessControls[0].Condition.Signature.Count = "1"
|
||||
|
||||
cfgBody, err := yaml.Marshal(h.cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.stage(map[string]string{ConfigPath: string(cfgBody)})
|
||||
_, badHash := h.changeCommit("add toot user", h.cfg.Accounts[1].ID, newSig)
|
||||
|
||||
_, err = h.repo.NewChangeCommit("add toot user", h.cfg.Accounts[1].ID, newSig)
|
||||
if aclErr := (accessctl.ErrConditionSignatureUnsatisfied{}); !errors.As(err, &aclErr) {
|
||||
t.Fatalf("NewChangeCommit should have returned an ErrConditionSignatureUnsatisfied, but returned %v", err)
|
||||
if err := h.repo.VerifyChangeCommit(TrunkRefName, badHash); err == nil {
|
||||
t.Fatal("toot user shouldn't be able to add itself to config")
|
||||
}
|
||||
h.reset(hash, git.HardReset)
|
||||
|
||||
// now add with the root user, this should work.
|
||||
h.stage(map[string]string{ConfigPath: string(cfgBody)})
|
||||
_, hash = h.changeCommit("add toot user", h.cfg.Accounts[0].ID, h.sig)
|
||||
hashes = append(hashes, hash)
|
||||
|
||||
@ -152,7 +153,7 @@ func TestConfigChange(t *testing.T) {
|
||||
hashes = append(hashes, hash)
|
||||
|
||||
for i, hash := range hashes {
|
||||
if err := h.repo.VerifyChangeCommit(hash); err != nil {
|
||||
if err := h.repo.VerifyChangeCommit(TrunkRefName, hash); err != nil {
|
||||
t.Fatalf("commit %d (%v) should have been verified but wasn't: %v", i, hash, err)
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ type Account struct {
|
||||
// Config represents the structure of the main dehub configuration file, and is
|
||||
// used to marshal/unmarshal the yaml file.
|
||||
type Config struct {
|
||||
Accounts []Account `yaml:"accounts"`
|
||||
AccessControls []accessctl.AccessControl `yaml:"access_controls"`
|
||||
Accounts []Account `yaml:"accounts"`
|
||||
AccessControls []accessctl.BranchAccessControl `yaml:"access_controls"`
|
||||
}
|
||||
|
||||
func (r *Repo) loadConfig(fs fs.FS) (Config, error) {
|
||||
|
53
repo.go
53
repo.go
@ -12,6 +12,7 @@ import (
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||
"gopkg.in/src-d/go-git.v4/storage"
|
||||
"gopkg.in/src-d/go-git.v4/storage/memory"
|
||||
)
|
||||
|
||||
@ -25,8 +26,11 @@ var (
|
||||
// ConfigPath defines the expected path to the Repo's configuration file.
|
||||
ConfigPath = filepath.Join(DehubDir, "config.yml")
|
||||
|
||||
// Trunk defines the reference name of the trunk branch.
|
||||
Trunk = plumbing.ReferenceName("refs/heads/trunk")
|
||||
// Trunk defines the name of the trunk branch.
|
||||
Trunk = "trunk"
|
||||
|
||||
// TrunkRefName defines the reference name of the trunk branch.
|
||||
TrunkRefName = plumbing.NewBranchReferenceName(Trunk)
|
||||
)
|
||||
|
||||
type repoOpts struct {
|
||||
@ -48,6 +52,7 @@ func OpenBare(bare bool) OpenOption {
|
||||
// Repo is an object which allows accessing and modifying the dehub repo.
|
||||
type Repo struct {
|
||||
GitRepo *git.Repository
|
||||
Storer storage.Storer
|
||||
}
|
||||
|
||||
// OpenRepo opens the dehub repo in the given directory and returns the object
|
||||
@ -69,6 +74,7 @@ func OpenRepo(path string, options ...OpenOption) (*Repo, error) {
|
||||
if r.GitRepo, err = git.PlainOpenWithOptions(path, openOpts); err != nil {
|
||||
return nil, fmt.Errorf("could not open git repo: %w", err)
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
@ -79,7 +85,19 @@ func InitMemRepo() *Repo {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &Repo{GitRepo: r}
|
||||
repo := &Repo{GitRepo: r}
|
||||
if err := repo.init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return repo
|
||||
}
|
||||
|
||||
func (r *Repo) init() error {
|
||||
h := plumbing.NewSymbolicReference(plumbing.HEAD, TrunkRefName)
|
||||
if err := r.GitRepo.Storer.SetReference(h); err != nil {
|
||||
return fmt.Errorf("could not set HEAD to %q: %w", TrunkRefName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) billyFilesystem() (billy.Filesystem, error) {
|
||||
@ -90,6 +108,35 @@ func (r *Repo) billyFilesystem() (billy.Filesystem, error) {
|
||||
return w.Filesystem, nil
|
||||
}
|
||||
|
||||
// CheckedOutBranch returns the name of the currently checked out branch.
|
||||
func (r *Repo) CheckedOutBranch() (plumbing.ReferenceName, error) {
|
||||
// Head() can't be used for this, because it doesn't handle the case of a
|
||||
// newly initialized repo very well.
|
||||
ogRef, err := r.GitRepo.Storer.Reference(plumbing.HEAD)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't de-reference HEAD (is it a bare repo?): %w", err)
|
||||
}
|
||||
|
||||
ref := ogRef
|
||||
for {
|
||||
if ref.Type() != plumbing.SymbolicReference {
|
||||
break
|
||||
}
|
||||
|
||||
target := ref.Target()
|
||||
if target.IsBranch() {
|
||||
return target, nil
|
||||
}
|
||||
|
||||
ref, err = r.GitRepo.Storer.Reference(target)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not de-reference HEAD to a branch: %w", err)
|
||||
}
|
||||
|
||||
func (r *Repo) head() (*object.Commit, *object.Tree, error) {
|
||||
head, err := r.GitRepo.Head()
|
||||
if err != nil {
|
||||
|
87
repo_test.go
87
repo_test.go
@ -10,6 +10,7 @@ import (
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
@ -34,13 +35,18 @@ func newHarness(t *testing.T) *harness {
|
||||
Path: pubKeyPath,
|
||||
}}},
|
||||
}},
|
||||
AccessControls: []accessctl.AccessControl{
|
||||
AccessControls: []accessctl.BranchAccessControl{
|
||||
{
|
||||
Pattern: "**",
|
||||
Condition: accessctl.Condition{
|
||||
Signature: &accessctl.ConditionSignature{
|
||||
AccountIDs: []string{"root"},
|
||||
Count: "100%",
|
||||
BranchPattern: "**",
|
||||
ChangeAccessControls: []accessctl.ChangeAccessControl{
|
||||
{
|
||||
FilePathPattern: "**",
|
||||
Condition: accessctl.Condition{
|
||||
Signature: &accessctl.ConditionSignature{
|
||||
AccountIDs: []string{"root"},
|
||||
Count: "100%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -115,6 +121,21 @@ func (h *harness) changeCommit(msg, accountID string, sig sigcred.SignifierInter
|
||||
return tc, hash
|
||||
}
|
||||
|
||||
func (h *harness) reset(to plumbing.Hash, mode git.ResetMode) {
|
||||
w, err := h.repo.GitRepo.Worktree()
|
||||
if err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
|
||||
err = w.Reset(&git.ResetOptions{
|
||||
Commit: to,
|
||||
Mode: mode,
|
||||
})
|
||||
if err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasStagedChanges(t *testing.T) {
|
||||
harness := newHarness(t)
|
||||
assertHasStaged := func(expHasStaged bool) {
|
||||
@ -141,3 +162,57 @@ func TestHasStagedChanges(t *testing.T) {
|
||||
harness.changeCommit("second commit", "root", harness.sig)
|
||||
assertHasStaged(false)
|
||||
}
|
||||
|
||||
// TestOldConfig tests that having an older, now malformed, Config doesn't mess
|
||||
// with the current parsing, as long as the default access controls still work.
|
||||
func TestOldConfig(t *testing.T) {
|
||||
harness := newHarness(t)
|
||||
|
||||
// overwrite the currently staged config file with an older form
|
||||
harness.stage(map[string]string{ConfigPath: `
|
||||
---
|
||||
accounts:
|
||||
- id: root
|
||||
signifiers:
|
||||
- type: pgp_public_key_file
|
||||
path: ".dehub/root.asc"
|
||||
|
||||
access_controls:
|
||||
- pattern: "**"
|
||||
condition:
|
||||
type: signature
|
||||
account_ids:
|
||||
- root
|
||||
count: 0
|
||||
`})
|
||||
_, hash0 := harness.changeCommit("first commit, this is going great", "root", harness.sig)
|
||||
|
||||
// even though that access_controls doesn't actually require any signatures,
|
||||
// it should be used because it's not well formed.
|
||||
harness.stage(map[string]string{"foo": "no rules!"})
|
||||
_, hash1 := harness.changeCommit("ain't no laws", "toot", nil)
|
||||
|
||||
// verifying the first should work, but not the second.
|
||||
if err := harness.repo.VerifyChangeCommit(TrunkRefName, hash0); err != nil {
|
||||
t.Fatalf("first commit %q should be verifiable, but got: %v", hash0, err)
|
||||
} else if err := harness.repo.VerifyChangeCommit(TrunkRefName, hash1); err == nil {
|
||||
t.Fatalf("second commit %q should not have been verified", hash1)
|
||||
}
|
||||
|
||||
// reset back to hash0
|
||||
harness.reset(hash0, git.HardReset)
|
||||
|
||||
// make a commit fixing the config. everything should still be fine.
|
||||
harness.stage(map[string]string{ConfigPath: `
|
||||
---
|
||||
accounts:
|
||||
- id: root
|
||||
signifiers:
|
||||
- type: pgp_public_key_file
|
||||
path: ".dehub/root.asc"
|
||||
`})
|
||||
_, hash2 := harness.changeCommit("Fix the config!", "root", harness.sig)
|
||||
if err := harness.repo.VerifyChangeCommit(TrunkRefName, hash2); err != nil {
|
||||
t.Fatalf("config fix commit %q should be verifiable, but got: %v", hash2, err)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user