@ -2,10 +2,6 @@ package dehub
import (
import (
"bytes"
"bytes"
"dehub.dev/src/dehub.git/accessctl"
"dehub.dev/src/dehub.git/fs"
"dehub.dev/src/dehub.git/sigcred"
"dehub.dev/src/dehub.git/typeobj"
"encoding/base64"
"encoding/base64"
"errors"
"errors"
"fmt"
"fmt"
@ -14,6 +10,11 @@ import (
"strings"
"strings"
"time"
"time"
"dehub.dev/src/dehub.git/accessctl"
"dehub.dev/src/dehub.git/fs"
"dehub.dev/src/dehub.git/sigcred"
"dehub.dev/src/dehub.git/typeobj"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/object"
"gopkg.in/src-d/go-git.v4/plumbing/object"
@ -288,74 +289,49 @@ func (r *Repo) HasStagedChanges() (bool, error) {
return any , nil
return any , nil
}
}
type verificationCtx struct {
// VerifyCommits verifies that the given commits, which are presumably on the
commit * object . Commit
// given branch, are gucci.
commitTree , parentTree * object . Tree
func ( r * Repo ) VerifyCommits ( branch plumbing . ReferenceName , gitCommits [ ] GitCommit ) error {
}
// non-gophers gonna hate on this method, but I say it's fine
func ( r * Repo ) verificationCtx ( h plumbing . Hash ) ( vctx verificationCtx , err error ) {
if vctx . commit , err = r . GitRepo . CommitObject ( h ) ; err != nil {
return vctx , fmt . Errorf ( "retrieving commit object: %w" , err )
} else if vctx . commitTree , err = r . GitRepo . TreeObject ( vctx . commit . TreeHash ) ; err != nil {
return vctx , fmt . Errorf ( "retrieving commit tree object %q: %w" ,
vctx . commit . TreeHash , err )
} else if parent , err := vctx . commit . Parent ( 0 ) ; err != nil {
return vctx , fmt . Errorf ( "retrieving commit parent: %w" , err )
} else if vctx . parentTree , err = r . GitRepo . TreeObject ( parent . TreeHash ) ; err != nil {
return vctx , fmt . Errorf ( "retrieving commit parent tree object %q: %w" ,
parent . Hash , err )
}
return vctx , nil
}
func ( r * Repo ) assertAccessControls (
// First determine the root of the main branch. All commits need to be an
acl [ ] accessctl . AccessControl ,
// ancestor of it.
commit Commit , vctx verificationCtx , branch plumbing . ReferenceName ,
var root plumbing . Hash
) ( err error ) {
mainGitCommit , err := r . GetGitRevision ( plumbing . Revision ( MainRefName ) )
filesChanged , err := calcDiff ( vctx . parentTree , vctx . commitTree )
if err != nil {
if err != nil {
return fmt . Errorf ( "calculating diff from tree %q to tree %q: %w" ,
return fmt . Errorf ( "retrieving commit at HEAD of main: %w" , err )
vctx . parentTree . Hash , vctx . commitTree . Hash , err )
} else if len ( filesChanged ) > 0 && commit . Change == nil {
return errors . New ( "files changes but commit is not a change commit" )
}
}
pathsChanged := make ( [ ] string , len ( filesChanged ) )
rootCommit := mainGitCommit . GitCommit
for i := range filesChanged {
for {
pathsChanged [ i ] = filesChanged [ i ] . path
if rootCommit . NumParents ( ) == 0 {
}
break
} else if rootCommit . NumParents ( ) > 1 {
commitType , err := commit . Type ( )
return fmt . Errorf ( "commit %q in main branch has more than one parent" , root )
if err != nil {
} else if rootCommit , err = rootCommit . Parent ( 0 ) ; err != nil {
return fmt . Errorf ( "determining type of commit %+v: %w" , commi t , err )
return fmt . Errorf ( "retrieving parent commit of %q: %w" , root , err )
}
}
return accessctl . AssertCanCommit ( acl , accessctl . CommitRequest {
Type : commitType ,
Branch : branch . Short ( ) ,
Credentials : commit . Common . Credentials ,
FilesChanged : pathsChanged ,
} )
}
}
// VerifyCommits verifies that the given commits, which are presumably on the
// given branch, are gucci.
func ( r * Repo ) VerifyCommits ( branch plumbing . ReferenceName , gitCommits [ ] GitCommit ) error {
for i , gitCommit := range gitCommits {
for i , gitCommit := range gitCommits {
// It's not a requirement that the given GitCommits are in ancestral
// It's not a requirement that the given GitCommits are in ancestral
// order, but usually they are, so we can help verifyCommit not have to
// order, but usually they are, so we can help verifyCommit not have to
// calculate the parentTree if the previous commit is the parent of this
// calculate the parentTree if the previous commit is the parent of this
// one.
// one, and not have to determine that each commit is an ancestor of
// main manually.
var parentTree * object . Tree
var parentTree * object . Tree
if i > 0 && gitCommits [ i - 1 ] . GitCommit . Hash == gitCommit . GitCommit . ParentHashes [ 0 ] {
if i > 0 && gitCommits [ i - 1 ] . GitCommit . Hash == gitCommit . GitCommit . ParentHashes [ 0 ] {
parentTree = gitCommits [ i - 1 ] . GitTree
parentTree = gitCommits [ i - 1 ] . GitTree
} else if gitCommit . GitCommit . Hash == rootCommit . Hash {
// looking at the root commit itself, assume it's ok
} else if isAncestor , err := rootCommit . IsAncestor ( gitCommit . GitCommit ) ; err != nil {
return fmt . Errorf ( "determining if %q is an ancestor of %q (root of main): %w" ,
gitCommit . GitCommit . Hash , rootCommit . Hash , err )
} else if ! isAncestor {
return fmt . Errorf ( "%q is not an ancestor of %q (root of main)" ,
gitCommit . GitCommit . Hash , rootCommit . Hash )
}
}
if err := r . verifyCommit ( branch , gitCommit , parentTree ) ; err != nil {
if err := r . verifyCommit ( branch , gitCommit , parentTree ) ; err != nil {
@ -394,17 +370,11 @@ func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit,
return fmt . Errorf ( "retrieving parent tree of commit: %w" , err )
return fmt . Errorf ( "retrieving parent tree of commit: %w" , err )
}
}
vctx := verificationCtx {
commit : gitCommit . GitCommit ,
commitTree : gitCommit . GitTree ,
parentTree : parentTree ,
}
var sigFS fs . FS
var sigFS fs . FS
if gitCommit . Root ( ) {
if gitCommit . Root ( ) {
sigFS = fs . FromTree ( vctx . comm itTree)
sigFS = fs . FromTree ( gitCommit . GitTree )
} else {
} else {
sigFS = fs . FromTree ( vctx . parentTree )
sigFS = fs . FromTree ( parentTree )
}
}
cfg , err := r . loadConfig ( sigFS )
cfg , err := r . loadConfig ( sigFS )
@ -413,26 +383,53 @@ func (r *Repo) verifyCommit(branch plumbing.ReferenceName, gitCommit GitCommit,
gitCommit . GitCommit . ParentHashes [ 0 ] , err )
gitCommit . GitCommit . ParentHashes [ 0 ] , err )
}
}
err = r . assertAccessControls ( cfg . AccessControls , gitCommit . Commit , vctx , branch )
// assert access controls
filesChanged , err := calcDiff ( parentTree , gitCommit . GitTree )
if err != nil {
return fmt . Errorf ( "calculating diff from tree %q to tree %q: %w" ,
parentTree . Hash , gitCommit . GitTree . Hash , err )
} else if len ( filesChanged ) > 0 && gitCommit . Commit . Change == nil {
return errors . New ( "files changes but commit is not a change commit" )
}
pathsChanged := make ( [ ] string , len ( filesChanged ) )
for i := range filesChanged {
pathsChanged [ i ] = filesChanged [ i ] . path
}
commitType , err := gitCommit . Commit . Type ( )
if err != nil {
return fmt . Errorf ( "determining type of commit %+v: %w" , gitCommit . Commit , err )
}
err = accessctl . AssertCanCommit ( cfg . AccessControls , accessctl . CommitRequest {
Type : commitType ,
Branch : branch . Short ( ) ,
Credentials : gitCommit . Commit . Common . Credentials ,
FilesChanged : pathsChanged ,
} )
if err != nil {
if err != nil {
return fmt . Errorf ( "enforcing access controls: %w" , err )
return fmt . Errorf ( "assert ing access controls: %w" , err )
}
}
changeHash := gitCommit . Interface . GetHash ( )
// ensure the hash is what it's expected to be
expectedChangeHash , err := gitCommit . Interface . Hash ( vctx . parentTree , vctx . commitTree )
commitHash := gitCommit . Interface . GetHash ( )
expectedCommitHash , err := gitCommit . Interface . Hash ( parentTree , gitCommit . GitTree )
if err != nil {
if err != nil {
return fmt . Errorf ( "calculating expected change hash: %w" , err )
return fmt . Errorf ( "calculating expected commit hash: %w" , err )
} else if ! bytes . Equal ( changeHash , expectedChangeHash ) {
} else if ! bytes . Equal ( commitHash , expectedCommit Hash ) {
return fmt . Errorf ( "malformed change_hash in commit body, is %s but should be %s" ,
return fmt . Errorf ( "unexpected hash in commit body, is %s but should be %s" ,
base64 . StdEncoding . EncodeToString ( expectedChangeHash ) ,
base64 . StdEncoding . EncodeToString ( expectedCommit Hash ) ,
base64 . StdEncoding . EncodeToString ( changeHash ) )
base64 . StdEncoding . EncodeToString ( commit Hash ) )
}
}
// verify all credentials
for _ , cred := range gitCommit . Commit . Common . Credentials {
for _ , cred := range gitCommit . Commit . Common . Credentials {
sig , err := r . signifierForCredential ( sigFS , cred )
sig , err := r . signifierForCredential ( sigFS , cred )
if err != nil {
if err != nil {
return fmt . Errorf ( "finding signifier for credential %+v: %w" , cred , err )
return fmt . Errorf ( "finding signifier for credential %+v: %w" , cred , err )
} else if err := sig . Verify ( sigFS , expectedChangeHash , cred ) ; err != nil {
} else if err := sig . Verify ( sigFS , expectedCommit Hash , cred ) ; err != nil {
return fmt . Errorf ( "verifying credential %+v: %w" , cred , err )
return fmt . Errorf ( "verifying credential %+v: %w" , cred , err )
}
}
}
}