76309b51cb
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
219 lines
5.4 KiB
Go
219 lines
5.4 KiB
Go
package dehub
|
|
|
|
import (
|
|
"bytes"
|
|
"dehub/accessctl"
|
|
"dehub/sigcred"
|
|
"io"
|
|
"math/rand"
|
|
"path/filepath"
|
|
"runtime/debug"
|
|
"testing"
|
|
|
|
"gopkg.in/src-d/go-git.v4"
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
yaml "gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type harness struct {
|
|
t *testing.T
|
|
rand *rand.Rand
|
|
repo *Repo
|
|
cfg *Config
|
|
sig sigcred.SignifierInterface
|
|
}
|
|
|
|
func newHarness(t *testing.T) *harness {
|
|
rand := rand.New(rand.NewSource(0xb4eadb01))
|
|
sig, pubKeyBody := sigcred.SignifierPGPTmp(rand)
|
|
pubKeyPath := filepath.Join(DehubDir, "root.asc")
|
|
|
|
cfg := &Config{
|
|
Accounts: []Account{{
|
|
ID: "root",
|
|
Signifiers: []sigcred.Signifier{{PGPPublicKeyFile: &sigcred.SignifierPGPFile{
|
|
Path: pubKeyPath,
|
|
}}},
|
|
}},
|
|
AccessControls: []accessctl.BranchAccessControl{
|
|
{
|
|
BranchPattern: "**",
|
|
ChangeAccessControls: []accessctl.ChangeAccessControl{
|
|
{
|
|
FilePathPattern: "**",
|
|
Condition: accessctl.Condition{
|
|
Signature: &accessctl.ConditionSignature{
|
|
AccountIDs: []string{"root"},
|
|
Count: "100%",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
cfgBody, err := yaml.Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
h := &harness{
|
|
t: t,
|
|
rand: rand,
|
|
repo: InitMemRepo(),
|
|
cfg: cfg,
|
|
sig: sig,
|
|
}
|
|
h.stage(map[string]string{
|
|
ConfigPath: string(cfgBody),
|
|
pubKeyPath: string(pubKeyBody),
|
|
})
|
|
|
|
return h
|
|
}
|
|
|
|
func (h *harness) stage(tree map[string]string) {
|
|
w, err := h.repo.GitRepo.Worktree()
|
|
if err != nil {
|
|
h.t.Fatal(err)
|
|
}
|
|
fs := w.Filesystem
|
|
for path, content := range tree {
|
|
if content == "" {
|
|
if _, err := w.Remove(path); err != nil {
|
|
h.t.Fatalf("error removing %q: %v", path, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
dir := filepath.Dir(path)
|
|
if err := fs.MkdirAll(dir, 0666); err != nil {
|
|
h.t.Fatalf("error making directory %q: %v", dir, err)
|
|
}
|
|
|
|
f, err := fs.Create(path)
|
|
if err != nil {
|
|
h.t.Fatalf("error creating file %q: %v", path, err)
|
|
|
|
} else if _, err := io.Copy(f, bytes.NewBufferString(content)); err != nil {
|
|
h.t.Fatalf("error writing to file %q: %v", path, err)
|
|
|
|
} else if err := f.Close(); err != nil {
|
|
h.t.Fatalf("error closing file %q: %v", path, err)
|
|
|
|
} else if _, err := w.Add(path); err != nil {
|
|
h.t.Fatalf("error adding file %q to index: %v", path, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *harness) changeCommit(msg, accountID string, sig sigcred.SignifierInterface) (ChangeCommit, plumbing.Hash) {
|
|
tc, err := h.repo.NewChangeCommit(msg, accountID, sig)
|
|
if err != nil {
|
|
h.t.Fatalf("failed to make ChangeCommit: %v", err)
|
|
}
|
|
|
|
hash, err := h.repo.Commit(tc, accountID)
|
|
if err != nil {
|
|
h.t.Fatalf("failed to commit ChangeCommit: %v", err)
|
|
}
|
|
|
|
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) {
|
|
hasStaged, err := harness.repo.HasStagedChanges()
|
|
if err != nil {
|
|
debug.PrintStack()
|
|
t.Fatalf("error calling HasStagedChanges: %v", err)
|
|
} else if hasStaged != expHasStaged {
|
|
debug.PrintStack()
|
|
t.Fatalf("expected HasStagedChanges to return %v", expHasStaged)
|
|
}
|
|
}
|
|
|
|
// the harness starts with some staged changes
|
|
assertHasStaged(true)
|
|
|
|
harness.stage(map[string]string{"foo": "bar"})
|
|
assertHasStaged(true)
|
|
harness.changeCommit("first commit", "root", harness.sig)
|
|
assertHasStaged(false)
|
|
|
|
harness.stage(map[string]string{"foo": ""}) // delete foo
|
|
assertHasStaged(true)
|
|
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)
|
|
}
|
|
}
|