326de2afc6
--- type: change message: |- Fully implement credential commits The actual commit objects and related refactoring had already been done, this commit takes the next step of implementing the access control changes, tests for verification, and refactoring of the dehub command to support multiple commit message types (as well as a small fix to dcmd). change_hash: AJyuAR0koVoe+uPBisa5qXsbW8YhlgOKNhnvy9uv7hQ8 credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl5tVzoACgkQlcRvpqQRSKyznw//b9lWd4V4G81cFwGAxZtJ3JiFpspYdtTAUUcLi9nogGsDmqkkSQxLdmBCT99QtaenKsxpad+9sXhkZpgWF/AyCX9pN6TTlMKuRcDXeoMUjeKjRpRhCHN0Lt8Sz80NDPYIa81r9cH0o1987GirgGmDEkYNDAFPDdGNDcCad/LLnG+ONwOl9WEM1q5O4etUPurTywlBiELDjHxeLzqgxCo8fMaMejW6mxnMDV6DIHiX6INWZAAG66HPVetmq6EVl9bnFgZmgKNzqKzFVZJRdGQNbhR/WzlOh0HPyJGwCveZPM5Zjd/dpfQUYEGGprVKc0G0YVNU2Hcz6O7hqafGGxWpCFW6zKrNmBRaW2u2zjVJD4ukmWn9gFuKJKhs0kyawRTbHNIX+gonYv9lDFO3cZ5qcsJbSAYSHrCav121z0GsQDoFJMJDQnP0syEEbAaxdQe7Bd7bmOM3SpCOLJLF1+X7Srrq5//u6fiFDxQ82Ylo3hG/r7/QT/vSipUCglx4POq33+z8VEHGhVfl4dgSU6OgIV/S7evKC7EiS/jh/xywU44RHpxFhwS3hthHxZqgRIHTm65DqGYWWZds2Hkr29TTRajuf0t4MxqY2MrLAhNJUc6OmrVN+lWMmm/z1FEhfrOvZ8v7mOSqTKwkvbsZzk5mpeo2RrLdNnnWvTCy87FpA48= account: mediocregopher
323 lines
7.6 KiB
Go
323 lines
7.6 KiB
Go
package dehub
|
|
|
|
import (
|
|
"bytes"
|
|
"dehub/accessctl"
|
|
"dehub/sigcred"
|
|
"errors"
|
|
"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("root", 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) stageCfg() {
|
|
cfgBody, err := yaml.Marshal(h.cfg)
|
|
if err != nil {
|
|
h.t.Fatal(err)
|
|
}
|
|
h.stage(map[string]string{ConfigPath: string(cfgBody)})
|
|
}
|
|
|
|
func (h *harness) checkout(branch plumbing.ReferenceName) {
|
|
w, err := h.repo.GitRepo.Worktree()
|
|
if err != nil {
|
|
h.t.Fatal(err)
|
|
}
|
|
|
|
head, _, err := h.repo.head()
|
|
if err != nil {
|
|
h.t.Fatal(err)
|
|
}
|
|
|
|
_, err = h.repo.GitRepo.Branch(branch.Short())
|
|
if errors.Is(err, git.ErrBranchNotFound) {
|
|
err = w.Checkout(&git.CheckoutOptions{
|
|
Hash: head.Hash,
|
|
Branch: branch,
|
|
Create: true,
|
|
})
|
|
} else if err != nil {
|
|
h.t.Fatalf("checking if branch already exists: %v", branch)
|
|
} else {
|
|
err = w.Checkout(&git.CheckoutOptions{
|
|
Branch: branch,
|
|
})
|
|
}
|
|
|
|
if err != nil {
|
|
h.t.Fatalf("checking out branch: %v", err)
|
|
}
|
|
}
|
|
|
|
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 (h *harness) tryCommit(
|
|
shouldSucceed bool,
|
|
commit Commit,
|
|
accountID string, accountSig sigcred.SignifierInterface,
|
|
) (
|
|
Commit, plumbing.Hash,
|
|
) {
|
|
if accountSig != nil {
|
|
var err error
|
|
if commit, err = h.repo.AccreditCommit(commit, accountSig); err != nil {
|
|
h.t.Fatalf("accrediting commit: %v", err)
|
|
}
|
|
}
|
|
|
|
hash, err := h.repo.Commit(commit, accountID)
|
|
if err != nil {
|
|
h.t.Fatalf("failed to commit ChangeCommit: %v", err)
|
|
}
|
|
|
|
branch, err := h.repo.CheckedOutBranch()
|
|
if err != nil {
|
|
h.t.Fatalf("determining checked out branch: %v", err)
|
|
}
|
|
|
|
err = h.repo.VerifyCommit(branch, hash)
|
|
if shouldSucceed && err != nil {
|
|
h.t.Fatalf("verifying commit %q: %v", hash, err)
|
|
} else if shouldSucceed {
|
|
return commit, hash
|
|
} else if !shouldSucceed && err == nil {
|
|
h.t.Fatalf("verifying commit %q should have failed", hash)
|
|
}
|
|
|
|
// commit verifying didn't succeed, reset it back. first get parent commit
|
|
// to reset to
|
|
commitObj, err := h.repo.GitRepo.CommitObject(hash)
|
|
if err != nil {
|
|
h.t.Fatalf("getting commit object of unverifiable hash %q: %v", hash, err)
|
|
} else if commitObj.NumParents() == 0 {
|
|
h.t.Fatalf("unverifiable commit %q has no parents, but it should", hash)
|
|
}
|
|
|
|
h.reset(commitObj.ParentHashes[0], git.HardReset)
|
|
return commit, hash
|
|
}
|
|
|
|
func (h *harness) changeCommit(
|
|
msg string,
|
|
accountID string,
|
|
sig sigcred.SignifierInterface,
|
|
) (
|
|
Commit, plumbing.Hash,
|
|
) {
|
|
commit, err := h.repo.NewCommitChange(msg)
|
|
if err != nil {
|
|
h.t.Fatalf("creating ChangeCommit: %v", err)
|
|
}
|
|
return h.tryCommit(true, commit, accountID, sig)
|
|
}
|
|
|
|
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
|
|
`})
|
|
|
|
// this commit should be created and verify fine
|
|
harness.changeCommit("first commit, this is going great", "root", harness.sig)
|
|
|
|
// this commit should not be verifiable, because toot isn't in accounts and
|
|
// the default access controls should be being used
|
|
harness.stage(map[string]string{"foo": "no rules!"})
|
|
badCommit, err := harness.repo.NewCommitChange("ain't no laws")
|
|
if err != nil {
|
|
t.Fatalf("creating CommitChange: %v", err)
|
|
}
|
|
harness.tryCommit(false, badCommit, "toot", nil)
|
|
|
|
// 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"
|
|
`})
|
|
harness.changeCommit("Fix the config!", "root", harness.sig)
|
|
}
|
|
|
|
// TestThisRepoStillVerifies opens this actual repository and ensures that all
|
|
// commits in it still verify, given this codebase.
|
|
func TestThisRepoStillVerifies(t *testing.T) {
|
|
repo, err := OpenRepo(".")
|
|
if err != nil {
|
|
t.Fatalf("error opening repo: %v", err)
|
|
}
|
|
|
|
headCommit, _, err := repo.head()
|
|
if err != nil {
|
|
t.Fatalf("error getting repo head: %v", err)
|
|
}
|
|
|
|
checkedOutBranch, err := repo.CheckedOutBranch()
|
|
if err != nil {
|
|
t.Fatalf("error determining checked out branch: %v", err)
|
|
}
|
|
|
|
for _, hash := range headCommit.ParentHashes {
|
|
if err := repo.VerifyCommit(checkedOutBranch, hash); err != nil {
|
|
t.Fatalf("error verifying commit %q of branch %q: %v", hash, checkedOutBranch, err)
|
|
}
|
|
}
|
|
}
|