package dehub import ( "bytes" "dehub/accessctl" "dehub/fs" "dehub/sigcred" "dehub/yamlutil" "encoding/base64" "errors" "fmt" "strings" "time" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" yaml "gopkg.in/yaml.v2" ) // MasterCommit describes the structure of the object encoded into the git // message of a commit in the master branch. type MasterCommit struct { Message string `yaml:"message"` ChangeHash yamlutil.Blob `yaml:"change_hash"` Credentials []sigcred.Credential `yaml:"credentials"` } type mcYAML struct { Val MasterCommit `yaml:",inline"` } func msgHead(msg string) string { i := strings.Index(msg, "\n") if i > 0 { return msg[:i] } return msg } // MarshalText implements the encoding.TextMarshaler interface by returning the // form the MasterCommit object takes in the git commit message. func (mc MasterCommit) MarshalText() ([]byte, error) { masterCommitEncoded, err := yaml.Marshal(mcYAML{mc}) if err != nil { return nil, fmt.Errorf("failed to encode MasterCommit message: %w", err) } fullMsg := msgHead(mc.Message) + "\n\n" + string(masterCommitEncoded) return []byte(fullMsg), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface by decoding a // MasterCommit object which has been encoded into a git commit message. func (mc *MasterCommit) UnmarshalText(msg []byte) error { i := bytes.Index(msg, []byte("\n")) if i < 0 { return fmt.Errorf("commit message %q is malformed", msg) } msgHead, msg := msg[:i], msg[i:] var mcy mcYAML if err := yaml.Unmarshal(msg, &mcy); err != nil { return fmt.Errorf("could not unmarshal MasterCommit message: %w", err) } *mc = mcy.Val if !strings.HasPrefix(mc.Message, string(msgHead)) { return errors.New("encoded MasterCommit is malformed, it might not be an encoded MasterCommit") } return nil } // CommitMaster constructs a MasterCommit using the given SignifierInterface to // create a Credential for it. It returns the commit's hash after having set it // to HEAD. // // TODO this method is a prototype and does not reflect the method's final form. func (r *Repo) CommitMaster(msg, accountID string, sig sigcred.SignifierInterface) (MasterCommit, plumbing.Hash, error) { _, headTree, err := r.head() if errors.Is(err, plumbing.ErrReferenceNotFound) { headTree = &object.Tree{} } else if err != nil { return MasterCommit{}, plumbing.ZeroHash, err } _, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo) if err != nil { return MasterCommit{}, plumbing.ZeroHash, 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 MasterCommit{}, plumbing.ZeroHash, err } cfg, err := r.loadConfig(sigFS) if err != nil { return MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("could not load config: %w", err) } changeHash := genChangeHash(nil, msg, headTree, stagedTree) cred, err := sig.Sign(sigFS, changeHash) if err != nil { return MasterCommit{}, plumbing.ZeroHash, 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 MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("commit would not satisfy access controls: %w", err) } masterCommit := MasterCommit{ Message: msg, ChangeHash: changeHash, Credentials: []sigcred.Credential{cred}, } masterCommitB, err := masterCommit.MarshalText() if err != nil { return masterCommit, plumbing.ZeroHash, err } w, err := r.GitRepo.Worktree() if err != nil { return masterCommit, plumbing.ZeroHash, fmt.Errorf("could not get git worktree: %w", err) } hash, err := w.Commit(string(masterCommitB), &git.CommitOptions{ Author: &object.Signature{ Name: accountID, When: time.Now(), }, }) if err != nil { return masterCommit, hash, fmt.Errorf("failed to commit changed: %w", err) } return masterCommit, hash, nil } func (r *Repo) assertAccessControls( accessCtls []accessctl.AccessControl, creds []sigcred.Credential, from, to *object.Tree, ) error { filesChanged, err := calcDiff(from, to) if err != nil { return err } pathsChanged := make([]string, len(filesChanged)) for i := range filesChanged { pathsChanged[i] = filesChanged[i].path } accessCtls, err = accessctl.ApplicableAccessControls(accessCtls, pathsChanged) if err != nil { return fmt.Errorf("could not determine applicable access controls: %w", err) } for _, accessCtl := range accessCtls { condInt, err := accessCtl.Condition.Interface() if err != nil { return fmt.Errorf("could not cast Condition to interface: %w", err) } else if err := condInt.Satisfied(creds); err != nil { return fmt.Errorf("access control for pattern %q not satisfied: %w", accessCtl.Pattern, err) } } return nil } // VerifyMasterCommit verifies that the commit at the given hash, which is // presumably on the master branch, is gucci. func (r *Repo) VerifyMasterCommit(h plumbing.Hash) error { commit, err := r.GitRepo.CommitObject(h) if err != nil { return fmt.Errorf("could not retrieve commit object: %w", err) } commitTree, err := r.GitRepo.TreeObject(commit.TreeHash) if err != nil { return fmt.Errorf("could not retrieve tree object: %w", err) } var masterCommit MasterCommit if err := masterCommit.UnmarshalText([]byte(commit.Message)); err != nil { return err } sigTree := commitTree // only for root commit parentTree := &object.Tree{} if commit.NumParents() > 0 { parent, err := commit.Parent(0) if err != nil { return fmt.Errorf("could not retrieve parent of commit: %w", err) } else if parentTree, err = r.GitRepo.TreeObject(parent.TreeHash); err != nil { return fmt.Errorf("could not retrieve tree object of parent %q: %w", parent.Hash, err) } sigTree = parentTree } sigFS := fs.FromTree(sigTree) cfg, err := r.loadConfig(sigFS) if err != nil { return fmt.Errorf("error loading config: %w", err) } err = r.assertAccessControls( cfg.AccessControls, masterCommit.Credentials, parentTree, commitTree, ) if err != nil { return fmt.Errorf("failed to satisfy all access controls: %w", err) } expectedChangeHash := genChangeHash(nil, masterCommit.Message, parentTree, commitTree) if !bytes.Equal(masterCommit.ChangeHash, expectedChangeHash) { return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s", base64.StdEncoding.EncodeToString(expectedChangeHash), base64.StdEncoding.EncodeToString(masterCommit.ChangeHash)) } for _, cred := range masterCommit.Credentials { sig, err := r.signifierForCredential(sigFS, cred) if err != nil { return fmt.Errorf("error finding signifier for credential %+v: %w", cred, err) } else if err := sig.Verify(sigFS, expectedChangeHash, cred); err != nil { return fmt.Errorf("error verifying credential %+v: %w", cred, err) } } // TODO access controls return nil }