Create the welcome thread, and a README for it
--- type: change description: Create the welcome thread, and a README for it fingerprint: ACfbSiTJmQ04DduNlyf0kNvJgqhGkJC1osSEZ9kdO6+o credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6tzlUACgkQlcRvpqQRSKyTYxAAjfPI881Xu168EJmwi2by9QLcUlcYY+t5DmJaxtGB+WT7W9qcfZ3WOwzST+X4rBvoA8oTPnfI6PE2tuF9RPgRBSxn3JOALRH2VwqoY5fsuTOk5/BO1uukPZdycdDpYZRKpQZKC8kzt3KwYskRR4CoxVroqmAzxEVba4dZTAXprov724cU7QXWXjOtU2iX0JNn/S/yX3L3g1v4sOVbaaUmif4aOLntx+7E2R7v28aBg0HL2uTgSs5nsHLXXfdRcm1CFmGzX8FNAChHkpUg9OdDpd5+mqBf7ymKBWuv0z+I2qe6xTPAshcMm3EWfbUpb1+Bux7UpywwZnz97HvdopFnKaHAfbv99Sfm/OqzgMeLClWv3Iysm1k5PcXobvfs2E9MUfIjG085jTZ0cq0OPqGhODkBOVHyn4Cm71ZMELt9yAkihxKLHjkp3J0WwQv0HbEieA0fE6Czmc481oTd0kGlDWTla/LMd3/vU4Gpx89Y9+2lTV0WaXoAawjJmEXQwqSCiPHYSnfAWgjDTkEAkNMHN8HgMYsVxhtipayvYPiWJFRhL5LVKGNgwUefTfhhhvrx1FBza5sF06XB7vKbb3npvZrfm2faLi1eyFX2xIl7m7dY6C4XYr3CBgEPiBh/NiCaZiOtjxOkzrJ/bsWNGolURMhNt9NAWFms8Nz5bXnRwZQ= account: mediocregopher
This commit is contained in:
parent
7c891bd5f2
commit
f5584f1505
@ -6,9 +6,15 @@ accounts:
|
|||||||
path: ".dehub/mediocregopher.asc"
|
path: ".dehub/mediocregopher.asc"
|
||||||
|
|
||||||
access_controls:
|
access_controls:
|
||||||
- pattern: "**"
|
- action: allow
|
||||||
condition:
|
filters:
|
||||||
type: signature
|
- type: branch
|
||||||
account_ids:
|
pattern: public/welcome
|
||||||
- mediocregopher
|
- type: payload_type
|
||||||
count: 100%
|
payload_type: comment
|
||||||
|
- type: not
|
||||||
|
filter:
|
||||||
|
type: commit_attributes
|
||||||
|
non_fast_forward: true
|
||||||
|
- type: signature
|
||||||
|
any: true
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
dehub
|
/dehub
|
||||||
|
72
README.md
Normal file
72
README.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Welcome!
|
||||||
|
|
||||||
|
Hello! Welcome to the dehub project. You've found your way onto the welcome
|
||||||
|
branch. This branch is open for anyone to leave a comment commit on it, provided
|
||||||
|
they sign their commit with a PGP key.
|
||||||
|
|
||||||
|
## Viewing comments
|
||||||
|
|
||||||
|
If you've gotten this far then viewing comments is as easy as doing `git log`.
|
||||||
|
All commits will be shown from newest to oldest. You will only see the latest
|
||||||
|
snapshot of comments that you've pulled from the server. In order to update that
|
||||||
|
snapshot do:
|
||||||
|
|
||||||
|
```
|
||||||
|
git pull -f origin public/welcome
|
||||||
|
```
|
||||||
|
|
||||||
|
## Leaving a comment
|
||||||
|
|
||||||
|
The first step to leaving a comment of your own is to install dehub. Visit
|
||||||
|
`https://dehub.dev` for more on how to do that.
|
||||||
|
|
||||||
|
Once done, and assuming you have this branch checked out (how are you reading
|
||||||
|
this if you don't?), just do the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
dehub commit --anon-pgp-key=KEY_NAME comment
|
||||||
|
```
|
||||||
|
|
||||||
|
(`KEY_NAME` should be replaced with any selector which will match your pgp key,
|
||||||
|
such as the key ID, the name on the key, or the email.)
|
||||||
|
|
||||||
|
Your default text editor (defined by the `EDITOR` environment variable) will pop
|
||||||
|
up and you can then write down your comment. When you save and close your editor
|
||||||
|
dehub will sign the comment with your pgp key and create a commit with it.
|
||||||
|
|
||||||
|
You can view your newly created commit by calling `git show`.
|
||||||
|
|
||||||
|
If after you've created your comment commit (but before you've pushed it) you'd
|
||||||
|
like to amend it, do:
|
||||||
|
|
||||||
|
```
|
||||||
|
dehub commit --anon-pgp-key=KEY_NAME comment --amend
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, to push your comment commit up, you can do:
|
||||||
|
|
||||||
|
```
|
||||||
|
git push origin public/welcome
|
||||||
|
```
|
||||||
|
|
||||||
|
Once pushed, everyone will be able to see your comment!
|
||||||
|
|
||||||
|
### What to say?
|
||||||
|
|
||||||
|
Here's some starting points if you're not sure what to write in your first
|
||||||
|
comment:
|
||||||
|
|
||||||
|
* Introduce yourself; say where you're from and what your interests are.
|
||||||
|
* How did you find dehub? Why is it interesting to you?
|
||||||
|
* If you're using dehub for a project, shill your project!
|
||||||
|
* If you'd like to get involved in dehub's development, let us know what your
|
||||||
|
skills are and how you can help. Remember, it takes more than expert
|
||||||
|
programmers to make a project successful.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
Please be kind to others, and keep discussion related to dehub and
|
||||||
|
dehub-adjacent topics. Politics, in general, is not going to be related to
|
||||||
|
dehub. Comments which are off-topic or otherwise abusive are subject to being
|
||||||
|
removed.
|
||||||
|
|
195
SPEC.md
195
SPEC.md
@ -1,195 +0,0 @@
|
|||||||
# .dehub
|
|
||||||
|
|
||||||
The `.dehub` directory contains all meta information related to
|
|
||||||
decentralized repository management and access control.
|
|
||||||
|
|
||||||
## config.yml
|
|
||||||
|
|
||||||
The `.dehub/config.yml` file takes the following structure:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# accounts defines all accounts which are known to the repo.
|
|
||||||
accounts:
|
|
||||||
|
|
||||||
# Each account is an object with an id and at least one identifier. The id
|
|
||||||
# must be unique for each account.
|
|
||||||
- id: some_user_id:
|
|
||||||
|
|
||||||
# signifiers describes different methods the account might use to
|
|
||||||
# identify itself. Generally, these will be different public keys which
|
|
||||||
# commits will be signed with. At least one is required.
|
|
||||||
signifiers:
|
|
||||||
- type: "pgp_public_key"
|
|
||||||
body: "FULL PGP PUBLIC KEY STRING"
|
|
||||||
|
|
||||||
- type: "pgp_public_key_file"
|
|
||||||
path: ".dehub/some_user_id.asc"
|
|
||||||
|
|
||||||
- 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:
|
|
||||||
|
|
||||||
# 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/**"
|
|
||||||
|
|
||||||
# signature conditions indicate that a commit must be signed by one or
|
|
||||||
# more accounts to be allowed.
|
|
||||||
condition:
|
|
||||||
type: signature
|
|
||||||
|
|
||||||
# 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%
|
|
||||||
|
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
# Master commit
|
|
||||||
|
|
||||||
All new commits being appended to the HEAD of the `master` branch are subject to
|
|
||||||
the following requirements:
|
|
||||||
|
|
||||||
* Must conform to all requirements defined by the `access_controls` section of
|
|
||||||
the `config.yml`, as found in the HEAD. If the commit is the initial commit of
|
|
||||||
the repo then it instead uses the `config.yml` found in itself.
|
|
||||||
|
|
||||||
* Must not be a merge commit (this may be amended later, but at present it
|
|
||||||
simplifies implementation).
|
|
||||||
|
|
||||||
* The commit message must conform to the format and semantics defined below.
|
|
||||||
|
|
||||||
## Master Commit Message
|
|
||||||
|
|
||||||
The commit message for a commit being appended to the HEAD of the `master`
|
|
||||||
branch must conform to the following format: a single line (the message head)
|
|
||||||
giving a short description of the change, then two newlines, then a body which
|
|
||||||
is a yaml formatted string:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
This is the message head. It will be re-iterated within the yaml body.
|
|
||||||
|
|
||||||
# Now the yaml body begins
|
|
||||||
---
|
|
||||||
message: >
|
|
||||||
This is the message head. It will be re-iterated within the yaml body.
|
|
||||||
|
|
||||||
The rest of this field is for the message body, which corresponds to the
|
|
||||||
body of a normal commit message which might give a more long-form
|
|
||||||
explanation of the commit's changes.
|
|
||||||
|
|
||||||
Since the message is used in generating the signature it's necessary for it
|
|
||||||
to be encoded here fully formed, even though the message head is then
|
|
||||||
duplicated. Otherwise the exact bytes of the message would be ambiguous.
|
|
||||||
This situation is ugly, but not unbearable.
|
|
||||||
|
|
||||||
# See the Commit Signatures section below for how this is computed. The
|
|
||||||
# change_hash is always recomputed when verifying a commit, but is reproduced in
|
|
||||||
# the commit message itself for cases of forward compatibility, e.g. if the
|
|
||||||
algorithm to compute the hash changes.
|
|
||||||
change_hash: XXX
|
|
||||||
|
|
||||||
# Credentials are the set of credentials which count towards requirements
|
|
||||||
# specified in the `access_controls` section of the `config.yml` file.
|
|
||||||
credentials:
|
|
||||||
|
|
||||||
- type: pgp_signature
|
|
||||||
account_id: some_user_id
|
|
||||||
pub_key_id: XXX
|
|
||||||
body: "base-64 signature body"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commit Signatures
|
|
||||||
|
|
||||||
When a commit is being signed by a signifier there is an expected data format
|
|
||||||
for the data to be signed. The format is a SHA-256 hash of the following pieces
|
|
||||||
of data concatenated together (the "change_hash"):
|
|
||||||
|
|
||||||
* A uvarint indicating the number of bytes in the commit message.
|
|
||||||
* The message.
|
|
||||||
* A uvarint indicating the number of files changed.
|
|
||||||
* For each file changed in the commit, ordered lexographically-ascending based
|
|
||||||
on its full relative path within the repo, the following is then written:
|
|
||||||
* A uvarint indicating the length of the full relative path of the file
|
|
||||||
within the repo.
|
|
||||||
* The full relative path of the file within the repo.
|
|
||||||
* A little-endian uint32 representing the previous file mode of the file (or 0
|
|
||||||
if the file is being inserted).
|
|
||||||
* The 20-byte SHA1 hash of the previous version of the file's contents (or 20
|
|
||||||
0 bytes if the file is being inserted).
|
|
||||||
* A little-endian uint32 representing the new file mode of the file (or 0
|
|
||||||
if the file is being deleted).
|
|
||||||
* The 20-byte SHA1 hash of the new version of the file's contents (or 20
|
|
||||||
0 bytes if the file is being deleted).
|
|
||||||
|
|
||||||
The raw output from the SHA-256 is then prepended with a `0` byte (for forward
|
|
||||||
compatibility) and signed, and the result used as the signature body.
|
|
||||||
|
|
||||||
# Merge Requests
|
|
||||||
|
|
||||||
A merge request (MR) may be pushed to the repository as a new branch at any
|
|
||||||
time. All MR branch names follow the naming convention `DHMR-short-description`.
|
|
||||||
An MR branch has the following qualities:
|
|
||||||
|
|
||||||
* Meta commits (see sub-section) will only contain a commit message head/body,
|
|
||||||
but no file changes.
|
|
||||||
|
|
||||||
* The most recent substantial commit (as opposed to meta commits) should always
|
|
||||||
contain the full commit message head and body.
|
|
||||||
|
|
||||||
## Meta Commits
|
|
||||||
|
|
||||||
Meta commits are those which add information about the changes being requested,
|
|
||||||
but do not modify the changes themselves.
|
|
||||||
|
|
||||||
### Signature Commits
|
|
||||||
|
|
||||||
Signature commits sign the changes requested in order to count towards their
|
|
||||||
access control requirements. The message head of these are arbitrary, but the
|
|
||||||
body must be formatted as such:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# This object matches the one found in the `credentials` section of the master
|
|
||||||
# commit message.
|
|
||||||
type: pgp_signature
|
|
||||||
account_id: some_user_id ```
|
|
||||||
pub_key_id: XXX
|
|
||||||
body: "base-64 signature body" # see Commit Signatures sub-section.
|
|
||||||
```
|
|
||||||
|
|
||||||
If a signature commit is added to a MR branch, and a substantial commit is
|
|
||||||
added after it, then that signature commit will no longer be valid, as it was
|
|
||||||
only signing the the prior changeset. The signer will need to create and push a
|
|
||||||
new signature commit, if they agree with the new changes.
|
|
||||||
|
|
||||||
## Merging MRs
|
|
||||||
|
|
||||||
When an MR has accumulated enough meta commits to fulfuill access control
|
|
||||||
requirements it may be coalesced into a single commit destined for the master
|
|
||||||
branch. See the Master Commit Message sub-section for details on how commits in
|
|
||||||
the master branch must be formatted.
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
* access control patterns related to who may push to MR branches, and what types
|
|
||||||
of commits they can push.
|
|
@ -1,53 +0,0 @@
|
|||||||
package accessctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrNoApplicableAccessControls is returned from ApplicableAccessControls when
|
|
||||||
// a changed path has no applicable AccessControls which match it.
|
|
||||||
type ErrNoApplicableAccessControls struct {
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrNoApplicableAccessControls) Error() string {
|
|
||||||
return fmt.Sprintf("no AccessControls which apply to changed file %q", err.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
} else if ok {
|
|
||||||
applicableSet[ac] = struct{}{}
|
|
||||||
any = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !any {
|
|
||||||
return nil, ErrNoApplicableAccessControls{Path: path}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicable := make([]AccessControl, 0, len(applicableSet))
|
|
||||||
for ac := range applicableSet {
|
|
||||||
applicable = append(applicable, ac)
|
|
||||||
}
|
|
||||||
return applicable, nil
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
package accessctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestApplicableAccessControls(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
descr string
|
|
||||||
patterns, filesChanged []string
|
|
||||||
exp []string
|
|
||||||
expErrPath string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
descr: "empty input empty output",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "empty patterns",
|
|
||||||
filesChanged: []string{"foo", "bar"},
|
|
||||||
expErrPath: "foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "empty filesChanged",
|
|
||||||
patterns: []string{"patternA", "patternB"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "no applicable files",
|
|
||||||
filesChanged: []string{"foo"},
|
|
||||||
patterns: []string{"bar"},
|
|
||||||
expErrPath: "foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "all applicable files",
|
|
||||||
filesChanged: []string{"foo", "bar"},
|
|
||||||
patterns: []string{"**"},
|
|
||||||
exp: []string{"**"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "pattern precedent",
|
|
||||||
filesChanged: []string{"foo"},
|
|
||||||
patterns: []string{"foo", "**"},
|
|
||||||
exp: []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/**"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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]}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
package accessctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub/sigcred"
|
|
||||||
"dehub/typeobj"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConditionInterface describes the methods that all Signifiers must implement.
|
|
||||||
type ConditionInterface interface {
|
|
||||||
|
|
||||||
// Satisfied asserts that the Condition is satisfied by the given set of
|
|
||||||
// Credentials. If it is not (or something else went wrong) then an error is
|
|
||||||
// returned.
|
|
||||||
//
|
|
||||||
// NOTE that Satisfied assumes the Credential has already been Verify'd.
|
|
||||||
Satisfied([]sigcred.Credential) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Condition represents an access control condition being defined in the Config.
|
|
||||||
// Only one of its fields may be filled in at a time.
|
|
||||||
type Condition struct {
|
|
||||||
Signature *ConditionSignature `type:"signature"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalYAML implements the yaml.Marshaler interface.
|
|
||||||
func (c Condition) MarshalYAML() (interface{}, error) {
|
|
||||||
return typeobj.MarshalYAML(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
||||||
func (c *Condition) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
return typeobj.UnmarshalYAML(c, unmarshal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface returns the ConditionInterface encapsulated by this Condition
|
|
||||||
// object.
|
|
||||||
func (c Condition) Interface() (ConditionInterface, error) {
|
|
||||||
el, _, err := typeobj.Element(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return el.(ConditionInterface), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConditionSignature represents the configuration of an access control
|
|
||||||
// condition which requires one or more signatures to be present on a commit.
|
|
||||||
//
|
|
||||||
// Either AccountIDs or AccountIDsByMeta must be filled.
|
|
||||||
type ConditionSignature struct {
|
|
||||||
AccountIDs []string `yaml:"account_ids,omitempty"`
|
|
||||||
AnyAccount bool `yaml:"any_account,omitempty"`
|
|
||||||
Count string `yaml:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ConditionInterface = ConditionSignature{}
|
|
||||||
|
|
||||||
func (condSig ConditionSignature) targetNum() (int, error) {
|
|
||||||
if !strings.HasSuffix(condSig.Count, "%") {
|
|
||||||
return strconv.Atoi(condSig.Count)
|
|
||||||
} else if condSig.AnyAccount {
|
|
||||||
return 0, errors.New("cannot use AnyAccount and a percent Count together")
|
|
||||||
}
|
|
||||||
|
|
||||||
percentStr := strings.TrimRight(condSig.Count, "%")
|
|
||||||
percent, err := strconv.ParseFloat(percentStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("could not parse Count as percent %q: %w", condSig.Count, err)
|
|
||||||
}
|
|
||||||
targetF := float64(len(condSig.AccountIDs)) * percent / 100
|
|
||||||
targetF = math.Ceil(targetF)
|
|
||||||
return int(targetF), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrConditionSignatureUnsatisfied is returned from ConditionSignature's
|
|
||||||
// Satisfied method when the Condition has not been satisfied.
|
|
||||||
type ErrConditionSignatureUnsatisfied struct {
|
|
||||||
TargetNumAccounts, NumAccounts int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err ErrConditionSignatureUnsatisfied) Error() string {
|
|
||||||
return fmt.Sprintf("not enough valid signature credentials, requires %d but only had %d",
|
|
||||||
err.TargetNumAccounts, err.NumAccounts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Satisfied asserts that the given Credentials contains enough signatures to be
|
|
||||||
// satisfied.
|
|
||||||
func (condSig ConditionSignature) Satisfied(creds []sigcred.Credential) error {
|
|
||||||
targetN, err := condSig.targetNum()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not compute ConditionSignature target number of accounts: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
credAccountIDs := map[string]struct{}{}
|
|
||||||
for _, cred := range creds {
|
|
||||||
// TODO currently only signature credentials are implemented, so we can
|
|
||||||
// just assume that the given AccountID has provided a sig. In the
|
|
||||||
// future this may not be true.
|
|
||||||
credAccountIDs[cred.AccountID] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var n int
|
|
||||||
if condSig.AnyAccount {
|
|
||||||
// TODO this doesn't actually check that the accounts are defined in the
|
|
||||||
// Config.
|
|
||||||
n = len(credAccountIDs)
|
|
||||||
} else {
|
|
||||||
targetAccountIDs := map[string]struct{}{}
|
|
||||||
for _, accountID := range condSig.AccountIDs {
|
|
||||||
targetAccountIDs[accountID] = struct{}{}
|
|
||||||
}
|
|
||||||
for accountID := range targetAccountIDs {
|
|
||||||
if _, ok := credAccountIDs[accountID]; ok {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if n < targetN {
|
|
||||||
return ErrConditionSignatureUnsatisfied{
|
|
||||||
TargetNumAccounts: targetN,
|
|
||||||
NumAccounts: n,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,110 +0,0 @@
|
|||||||
package accessctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub/sigcred"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConditionSignatureSatisfied(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
descr string
|
|
||||||
cond ConditionSignature
|
|
||||||
credAccountIDs []string
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
descr: "no cred accounts",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AnyAccount: true,
|
|
||||||
Count: "1",
|
|
||||||
},
|
|
||||||
err: ErrConditionSignatureUnsatisfied{
|
|
||||||
TargetNumAccounts: 1,
|
|
||||||
NumAccounts: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "one cred account",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AnyAccount: true,
|
|
||||||
Count: "1",
|
|
||||||
},
|
|
||||||
credAccountIDs: []string{"foo"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "one matching cred account",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AccountIDs: []string{"foo", "bar"},
|
|
||||||
Count: "1",
|
|
||||||
},
|
|
||||||
credAccountIDs: []string{"foo"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "no matching cred account",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AccountIDs: []string{"foo", "bar"},
|
|
||||||
Count: "1",
|
|
||||||
},
|
|
||||||
credAccountIDs: []string{"baz"},
|
|
||||||
err: ErrConditionSignatureUnsatisfied{
|
|
||||||
TargetNumAccounts: 1,
|
|
||||||
NumAccounts: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "two matching cred accounts",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AccountIDs: []string{"foo", "bar"},
|
|
||||||
Count: "2",
|
|
||||||
},
|
|
||||||
credAccountIDs: []string{"foo", "bar"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "one matching cred account, missing one",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AccountIDs: []string{"foo", "bar"},
|
|
||||||
Count: "2",
|
|
||||||
},
|
|
||||||
credAccountIDs: []string{"foo", "baz"},
|
|
||||||
err: ErrConditionSignatureUnsatisfied{
|
|
||||||
TargetNumAccounts: 2,
|
|
||||||
NumAccounts: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "50 percent matching cred accounts",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AccountIDs: []string{"foo", "bar", "baz"},
|
|
||||||
Count: "50%",
|
|
||||||
},
|
|
||||||
credAccountIDs: []string{"foo", "bar"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "not 50 percent matching cred accounts",
|
|
||||||
cond: ConditionSignature{
|
|
||||||
AccountIDs: []string{"foo", "bar", "baz"},
|
|
||||||
Count: "50%",
|
|
||||||
},
|
|
||||||
credAccountIDs: []string{"foo"},
|
|
||||||
err: ErrConditionSignatureUnsatisfied{
|
|
||||||
TargetNumAccounts: 2,
|
|
||||||
NumAccounts: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.descr, func(t *testing.T) {
|
|
||||||
creds := make([]sigcred.Credential, len(test.credAccountIDs))
|
|
||||||
for i := range test.credAccountIDs {
|
|
||||||
creds[i].AccountID = test.credAccountIDs[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
err := test.cond.Satisfied(creds)
|
|
||||||
if !reflect.DeepEqual(err, test.err) {
|
|
||||||
t.Fatalf("Satisfied returned %#v\nexpected %#v", err, test.err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
package dehub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultHashHelperAlgo = sha256.New
|
|
||||||
)
|
|
||||||
|
|
||||||
type hashHelper struct {
|
|
||||||
hash.Hash
|
|
||||||
varintBuf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// if h is nil it then defaultHashHelperAlgo will be used
|
|
||||||
func newHashHelper(h hash.Hash) *hashHelper {
|
|
||||||
if h == nil {
|
|
||||||
h = defaultHashHelperAlgo()
|
|
||||||
}
|
|
||||||
s := &hashHelper{
|
|
||||||
Hash: h,
|
|
||||||
varintBuf: make([]byte, binary.MaxVarintLen64),
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *hashHelper) writeUint(i uint64) {
|
|
||||||
n := binary.PutUvarint(s.varintBuf, i)
|
|
||||||
if _, err := s.Write(s.varintBuf[:n]); err != nil {
|
|
||||||
panic(fmt.Sprintf("error writing %x to sha256 sum: %v", s.varintBuf[:n], err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *hashHelper) writeStr(str string) {
|
|
||||||
s.writeUint(uint64(len(str)))
|
|
||||||
s.Write([]byte(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *hashHelper) writeTreeDiff(from, to *object.Tree) {
|
|
||||||
filesChanged, err := calcDiff(from, to)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(filesChanged, func(i, j int) bool {
|
|
||||||
return filesChanged[i].path < filesChanged[j].path
|
|
||||||
})
|
|
||||||
|
|
||||||
s.writeUint(uint64(len(filesChanged)))
|
|
||||||
for _, fileChanged := range filesChanged {
|
|
||||||
s.writeStr(fileChanged.path)
|
|
||||||
s.Write(fileChanged.fromMode.Bytes())
|
|
||||||
s.Write(fileChanged.fromHash[:])
|
|
||||||
s.Write(fileChanged.toMode.Bytes())
|
|
||||||
s.Write(fileChanged.toHash[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var changeHashVersion = []byte{0}
|
|
||||||
|
|
||||||
// if h is nil it then defaultHashHelperAlgo will be used
|
|
||||||
func genChangeHash(h hash.Hash, msg string, from, to *object.Tree) []byte {
|
|
||||||
s := newHashHelper(h)
|
|
||||||
s.writeStr(msg)
|
|
||||||
s.writeTreeDiff(from, to)
|
|
||||||
return s.Sum(changeHashVersion)
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type subCmdCtx struct {
|
|
||||||
repo *dehub.Repo
|
|
||||||
args []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var subCmds = []struct {
|
|
||||||
name, descr string
|
|
||||||
body func(sctx subCmdCtx) error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "commit",
|
|
||||||
descr: "commits staged changes to the head of the current branch",
|
|
||||||
body: func(sctx subCmdCtx) error {
|
|
||||||
flag := flag.NewFlagSet("commit", flag.ExitOnError)
|
|
||||||
msg := flag.String("msg", "", "Commit message to use")
|
|
||||||
accountID := flag.String("account-id", "", "Account to sign commit as")
|
|
||||||
flag.Parse(sctx.args)
|
|
||||||
|
|
||||||
if *msg == "" || *accountID == "" {
|
|
||||||
return errors.New("-msg and -account-id are both required")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := sctx.repo.LoadConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var account dehub.Account
|
|
||||||
var ok bool
|
|
||||||
for _, account = range cfg.Accounts {
|
|
||||||
if account.ID == *accountID {
|
|
||||||
ok = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("account ID %q not found in config", *accountID)
|
|
||||||
} else if l := len(account.Signifiers); l == 0 || l > 1 {
|
|
||||||
return fmt.Errorf("account %q has %d signifiers, only one is supported right now", *accountID, l)
|
|
||||||
}
|
|
||||||
|
|
||||||
sig := account.Signifiers[0]
|
|
||||||
sigInt, err := sig.Interface()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, hash, err := sctx.repo.CommitMaster(*msg, *accountID, sigInt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Printf("changes committed to HEAD as %s\n", hash)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "verify",
|
|
||||||
descr: "verifies one or more commits as having the proper credentials",
|
|
||||||
body: func(sctx subCmdCtx) error {
|
|
||||||
flag := flag.NewFlagSet("verify", flag.ExitOnError)
|
|
||||||
rev := flag.String("rev", "HEAD", "Revision of commit to verify")
|
|
||||||
flag.Parse(sctx.args)
|
|
||||||
|
|
||||||
h, err := sctx.repo.GitRepo.ResolveRevision(plumbing.Revision(*rev))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not resolve revision %q: %w", *rev, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sctx.repo.VerifyMasterCommit(*h); err != nil {
|
|
||||||
return fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("commit at %q (%s) is good to go!\n", *rev, *h)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func printHelp() {
|
|
||||||
fmt.Printf("USAGE: %s <command> [-h]\n\n", os.Args[0])
|
|
||||||
fmt.Println("COMMANDS")
|
|
||||||
for _, subCmd := range subCmds {
|
|
||||||
fmt.Printf("\t%s : %s\n", subCmd.name, subCmd.descr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func exitErr(err error) {
|
|
||||||
fmt.Fprintf(os.Stderr, "exiting: %v\n", err)
|
|
||||||
os.Stderr.Sync()
|
|
||||||
os.Stdout.Sync()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) < 2 {
|
|
||||||
printHelp()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subCmdName := strings.ToLower(os.Args[1])
|
|
||||||
for _, subCmd := range subCmds {
|
|
||||||
if subCmd.name != subCmdName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := dehub.OpenRepo(".")
|
|
||||||
if err != nil {
|
|
||||||
exitErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = subCmd.body(subCmdCtx{
|
|
||||||
repo: r,
|
|
||||||
args: os.Args[2:],
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
exitErr(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("unknown command %q\n\n", subCmdName)
|
|
||||||
printHelp()
|
|
||||||
}
|
|
251
commit.go
251
commit.go
@ -1,251 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
170
commit_test.go
170
commit_test.go
@ -1,170 +0,0 @@
|
|||||||
package dehub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub/accessctl"
|
|
||||||
"dehub/sigcred"
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMasterCommitVerify(t *testing.T) {
|
|
||||||
type step struct {
|
|
||||||
msg string
|
|
||||||
msgHead string // defaults to msg
|
|
||||||
tree map[string]string
|
|
||||||
}
|
|
||||||
testCases := []struct {
|
|
||||||
descr string
|
|
||||||
steps []step
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
descr: "single commit",
|
|
||||||
steps: []step{
|
|
||||||
{
|
|
||||||
msg: "first commit",
|
|
||||||
tree: map[string]string{"a": "0", "b": "1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "multiple commits",
|
|
||||||
steps: []step{
|
|
||||||
{
|
|
||||||
msg: "first commit",
|
|
||||||
tree: map[string]string{"a": "0", "b": "1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
msg: "second commit, changing a",
|
|
||||||
tree: map[string]string{"a": "1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
msg: "third commit, empty",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
msg: "fourth commit, adding c, removing b",
|
|
||||||
tree: map[string]string{"b": "", "c": "2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "big body commits",
|
|
||||||
steps: []step{
|
|
||||||
{
|
|
||||||
msg: "first commit, single line but with newline\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
msg: "second commit, single line but with two newlines\n\n",
|
|
||||||
msgHead: "second commit, single line but with two newlines\n\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
msg: "third commit, multi-line with one newline\nanother line!",
|
|
||||||
msgHead: "third commit, multi-line with one newline\n\n",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
msg: "fourth commit, multi-line with two newlines\n\nanother line!",
|
|
||||||
msgHead: "fourth commit, multi-line with two newlines\n\n",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.descr, func(t *testing.T) {
|
|
||||||
h := newHarness(t)
|
|
||||||
for _, step := range test.steps {
|
|
||||||
h.stage(step.tree)
|
|
||||||
account := h.cfg.Accounts[0]
|
|
||||||
|
|
||||||
masterCommit, hash, err := h.repo.CommitMaster(step.msg, account.ID, h.sig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to make MasterCommit: %v", err)
|
|
||||||
} else if err := h.repo.VerifyMasterCommit(hash); err != nil {
|
|
||||||
t.Fatalf("could not verify hash %v: %v", hash, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := h.repo.GitRepo.CommitObject(hash)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to retrieve commit %v: %v", hash, err)
|
|
||||||
} else if step.msgHead == "" {
|
|
||||||
step.msgHead = strings.TrimSpace(step.msg) + "\n\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(commit.Message, step.msgHead) {
|
|
||||||
t.Fatalf("commit message %q does not start with expected head %q", commit.Message, step.msgHead)
|
|
||||||
}
|
|
||||||
|
|
||||||
var actualMasterCommit MasterCommit
|
|
||||||
if err := actualMasterCommit.UnmarshalText([]byte(commit.Message)); err != nil {
|
|
||||||
t.Fatalf("error unmarshaling commit body: %v", err)
|
|
||||||
} else if !reflect.DeepEqual(actualMasterCommit, masterCommit) {
|
|
||||||
t.Fatalf("returned master commit:\n%s\ndoes not match actual one:\n%s",
|
|
||||||
spew.Sdump(masterCommit), spew.Sdump(actualMasterCommit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigChange(t *testing.T) {
|
|
||||||
h := newHarness(t)
|
|
||||||
|
|
||||||
var hashes []plumbing.Hash
|
|
||||||
|
|
||||||
// commit the initial staged changes, which merely include the config and
|
|
||||||
// public key
|
|
||||||
_, hash, err := h.repo.CommitMaster("commit configuration", h.cfg.Accounts[0].ID, h.sig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
newSig, newPubKeyBody := sigcred.SignifierPGPTmp(h.rand)
|
|
||||||
h.cfg.Accounts = append(h.cfg.Accounts, Account{
|
|
||||||
ID: "toot",
|
|
||||||
Signifiers: []sigcred.Signifier{{PGPPublicKey: &sigcred.SignifierPGP{
|
|
||||||
Body: string(newPubKeyBody),
|
|
||||||
}}},
|
|
||||||
})
|
|
||||||
h.cfg.AccessControls[0].Condition.Signature.AccountIDs = []string{"root", "toot"}
|
|
||||||
h.cfg.AccessControls[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)})
|
|
||||||
|
|
||||||
_, _, err = h.repo.CommitMaster("add toot user", h.cfg.Accounts[1].ID, newSig)
|
|
||||||
if aclErr := (accessctl.ErrConditionSignatureUnsatisfied{}); !errors.As(err, &aclErr) {
|
|
||||||
t.Fatalf("CommitMaster should have returned an ErrConditionSignatureUnsatisfied, but returned %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// now add with the root user, this should work.
|
|
||||||
_, hash, err = h.repo.CommitMaster("add toot user", h.cfg.Accounts[0].ID, h.sig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got an unexpected error committing with root: %v", err)
|
|
||||||
}
|
|
||||||
hashes = append(hashes, hash)
|
|
||||||
|
|
||||||
// _now_ the toot user should be able to do things.
|
|
||||||
h.stage(map[string]string{"foo/bar": "what a cool file"})
|
|
||||||
_, hash, err = h.repo.CommitMaster("add a cool file", h.cfg.Accounts[1].ID, newSig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got an unexpected error committing with toot: %v", err)
|
|
||||||
}
|
|
||||||
hashes = append(hashes, hash)
|
|
||||||
|
|
||||||
for i, hash := range hashes {
|
|
||||||
if err := h.repo.VerifyMasterCommit(hash); err != nil {
|
|
||||||
t.Fatalf("commit %d (%v) should have been verified but wasn't: %v", i, hash, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
83
config.go
83
config.go
@ -1,83 +0,0 @@
|
|||||||
package dehub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub/accessctl"
|
|
||||||
"dehub/fs"
|
|
||||||
"dehub/sigcred"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Account represents a single account defined in the Config.
|
|
||||||
type Account struct {
|
|
||||||
ID string `yaml:"id"`
|
|
||||||
Signifiers []sigcred.Signifier `yaml:"signifiers"`
|
|
||||||
Meta map[string]string `yaml:"meta,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) loadConfig(fs fs.FS) (Config, error) {
|
|
||||||
rc, err := fs.Open(ConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("could not open config.yml: %w", err)
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
|
|
||||||
var cfg Config
|
|
||||||
if err := yaml.NewDecoder(rc).Decode(&cfg); err != nil {
|
|
||||||
return cfg, fmt.Errorf("could not decode config.yml: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO validate Config
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadConfig loads the Config object from the HEAD of the repo, or directly
|
|
||||||
// from the filesystem if there is no HEAD yet.
|
|
||||||
func (r *Repo) LoadConfig() (Config, error) {
|
|
||||||
headFS, err := r.headOrRawFS()
|
|
||||||
if err != nil {
|
|
||||||
return Config{}, fmt.Errorf("error retrieving repo HEAD: %w", err)
|
|
||||||
}
|
|
||||||
return r.loadConfig(headFS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) signifierForCredential(fs fs.FS, cred sigcred.Credential) (sigcred.SignifierInterface, error) {
|
|
||||||
cfg, err := r.loadConfig(fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error loading config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var account Account
|
|
||||||
var ok bool
|
|
||||||
for _, account = range cfg.Accounts {
|
|
||||||
if account.ID == cred.AccountID {
|
|
||||||
ok = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("no account object for account id %q present in config", cred.AccountID)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, sig := range account.Signifiers {
|
|
||||||
if sigInt, err := sig.Interface(); err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting signifier index:%d to inteface: %w", i, err)
|
|
||||||
} else if ok, err := sigInt.Signed(fs, cred); err != nil {
|
|
||||||
return nil, fmt.Errorf("error checking if signfier index:%d signed credential: %w", i, err)
|
|
||||||
} else if ok {
|
|
||||||
return sigInt, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("no signifier found for credential")
|
|
||||||
}
|
|
41
diff.go
41
diff.go
@ -1,41 +0,0 @@
|
|||||||
package dehub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fileChanged struct {
|
|
||||||
path string
|
|
||||||
fromMode, toMode filemode.FileMode
|
|
||||||
fromHash, toHash plumbing.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcDiff(from, to *object.Tree) ([]fileChanged, error) {
|
|
||||||
|
|
||||||
changes, err := object.DiffTree(from, to)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not calculate tree diff: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filesChanged := make([]fileChanged, len(changes))
|
|
||||||
for i, change := range changes {
|
|
||||||
if from := change.From; from.Name != "" {
|
|
||||||
filesChanged[i].path = from.Name
|
|
||||||
filesChanged[i].fromMode = from.TreeEntry.Mode
|
|
||||||
filesChanged[i].fromHash = from.TreeEntry.Hash
|
|
||||||
}
|
|
||||||
if to := change.To; to.Name != "" {
|
|
||||||
if exPath := filesChanged[i].path; exPath != "" && exPath != to.Name {
|
|
||||||
panic(fmt.Sprintf("DiffTree entry changed path from %q to %q", exPath, to.Name))
|
|
||||||
}
|
|
||||||
filesChanged[i].path = to.Name
|
|
||||||
filesChanged[i].toMode = to.TreeEntry.Mode
|
|
||||||
filesChanged[i].toHash = to.TreeEntry.Hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filesChanged, nil
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/src-d/go-billy.v4"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/filemode"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/format/index"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
||||||
"gopkg.in/src-d/go-git.v4/storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This file is largely copied from the git-go project's worktree_commit.go @ v4.13.1
|
|
||||||
|
|
||||||
// buildTreeHelper converts a given index.Index file into multiple git objects
|
|
||||||
// reading the blobs from the given filesystem and creating the trees from the
|
|
||||||
// index structure. The created objects are pushed to a given Storer.
|
|
||||||
type buildTreeHelper struct {
|
|
||||||
fs billy.Filesystem
|
|
||||||
s storage.Storer
|
|
||||||
|
|
||||||
trees map[string]*object.Tree
|
|
||||||
entries map[string]*object.TreeEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildTree builds the tree objects and push its to the storer, the hash
|
|
||||||
// of the root tree is returned.
|
|
||||||
func (h *buildTreeHelper) BuildTree(idx *index.Index) (plumbing.Hash, error) {
|
|
||||||
const rootNode = ""
|
|
||||||
h.trees = map[string]*object.Tree{rootNode: {}}
|
|
||||||
h.entries = map[string]*object.TreeEntry{}
|
|
||||||
|
|
||||||
for _, e := range idx.Entries {
|
|
||||||
if err := h.commitIndexEntry(e); err != nil {
|
|
||||||
return plumbing.ZeroHash, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.copyTreeToStorageRecursive(rootNode, h.trees[rootNode])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *buildTreeHelper) commitIndexEntry(e *index.Entry) error {
|
|
||||||
parts := strings.Split(e.Name, "/")
|
|
||||||
|
|
||||||
var fullpath string
|
|
||||||
for _, part := range parts {
|
|
||||||
parent := fullpath
|
|
||||||
fullpath = path.Join(fullpath, part)
|
|
||||||
|
|
||||||
h.doBuildTree(e, parent, fullpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *buildTreeHelper) doBuildTree(e *index.Entry, parent, fullpath string) {
|
|
||||||
if _, ok := h.trees[fullpath]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := h.entries[fullpath]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
te := object.TreeEntry{Name: path.Base(fullpath)}
|
|
||||||
|
|
||||||
if fullpath == e.Name {
|
|
||||||
te.Mode = e.Mode
|
|
||||||
te.Hash = e.Hash
|
|
||||||
} else {
|
|
||||||
te.Mode = filemode.Dir
|
|
||||||
h.trees[fullpath] = &object.Tree{}
|
|
||||||
}
|
|
||||||
|
|
||||||
h.trees[parent].Entries = append(h.trees[parent].Entries, te)
|
|
||||||
}
|
|
||||||
|
|
||||||
type sortableEntries []object.TreeEntry
|
|
||||||
|
|
||||||
func (sortableEntries) sortName(te object.TreeEntry) string {
|
|
||||||
if te.Mode == filemode.Dir {
|
|
||||||
return te.Name + "/"
|
|
||||||
}
|
|
||||||
return te.Name
|
|
||||||
}
|
|
||||||
func (se sortableEntries) Len() int { return len(se) }
|
|
||||||
func (se sortableEntries) Less(i int, j int) bool { return se.sortName(se[i]) < se.sortName(se[j]) }
|
|
||||||
func (se sortableEntries) Swap(i int, j int) { se[i], se[j] = se[j], se[i] }
|
|
||||||
|
|
||||||
func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tree) (plumbing.Hash, error) {
|
|
||||||
sort.Sort(sortableEntries(t.Entries))
|
|
||||||
for i, e := range t.Entries {
|
|
||||||
if e.Mode != filemode.Dir && !e.Hash.IsZero() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
path := path.Join(parent, e.Name)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
e.Hash, err = h.copyTreeToStorageRecursive(path, h.trees[path])
|
|
||||||
if err != nil {
|
|
||||||
return plumbing.ZeroHash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Entries[i] = e
|
|
||||||
}
|
|
||||||
|
|
||||||
o := h.s.NewEncodedObject()
|
|
||||||
if err := t.Encode(o); err != nil {
|
|
||||||
return plumbing.ZeroHash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.s.SetEncodedObject(o)
|
|
||||||
}
|
|
100
fs/fs.go
100
fs/fs.go
@ -1,100 +0,0 @@
|
|||||||
// Package fs implements abstractions for interacting with a filesystem, either
|
|
||||||
// via a git tree, a staged index, or directly.
|
|
||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gopkg.in/src-d/go-billy.v4"
|
|
||||||
"gopkg.in/src-d/go-git.v4"
|
|
||||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FS is a simple interface for reading off a snapshot of a filesystem.
|
|
||||||
type FS interface {
|
|
||||||
Open(path string) (io.ReadCloser, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type treeFS struct {
|
|
||||||
tree *object.Tree
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromTree wraps a git tree object to implement the FS interface. All paths
|
|
||||||
// will be relative to the root of the tree.
|
|
||||||
func FromTree(t *object.Tree) FS {
|
|
||||||
return treeFS{tree: t}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gt treeFS) Open(path string) (io.ReadCloser, error) {
|
|
||||||
f, err := gt.tree.File(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.Blob.Reader()
|
|
||||||
}
|
|
||||||
|
|
||||||
type billyFS struct {
|
|
||||||
fs billy.Filesystem
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromBillyFilesystem wraps a billy.Filesystem to implement the FS interface.
|
|
||||||
// All paths will be relative to the filesystem's root.
|
|
||||||
func FromBillyFilesystem(bfs billy.Filesystem) FS {
|
|
||||||
return billyFS{fs: bfs}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bfs billyFS) Open(path string) (io.ReadCloser, error) {
|
|
||||||
return bfs.fs.Open(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromStagedChangesTree processes the current set of staged changes into a tree
|
|
||||||
// object, and returns an FS for that tree. All paths will be relative to the
|
|
||||||
// root of the git repo.
|
|
||||||
func FromStagedChangesTree(repo *git.Repository) (FS, *object.Tree, error) {
|
|
||||||
w, err := repo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("could not open git worktree: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
storer := repo.Storer
|
|
||||||
idx, err := storer.Index()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("could not open git staging index: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
th := &buildTreeHelper{
|
|
||||||
fs: w.Filesystem,
|
|
||||||
s: storer,
|
|
||||||
}
|
|
||||||
|
|
||||||
treeHash, err := th.BuildTree(idx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("could not build staging index tree: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tree, err := repo.TreeObject(treeHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("could not get staged tree object (%q): %w", treeHash, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return FromTree(tree), tree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stub is an implementation of FS based on a map of paths to the file contents
|
|
||||||
// at that path. Paths should be "clean" or they will not match with anything.
|
|
||||||
type Stub map[string][]byte
|
|
||||||
|
|
||||||
// Open implements the method for the FS interface.
|
|
||||||
func (s Stub) Open(path string) (io.ReadCloser, error) {
|
|
||||||
body, ok := s[path]
|
|
||||||
if !ok {
|
|
||||||
return nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
return ioutil.NopCloser(bytes.NewReader(body)), nil
|
|
||||||
}
|
|
12
go.mod
12
go.mod
@ -1,12 +0,0 @@
|
|||||||
module dehub
|
|
||||||
|
|
||||||
go 1.13
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/bmatcuk/doublestar v1.2.2
|
|
||||||
github.com/davecgh/go-spew v1.1.1
|
|
||||||
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17
|
|
||||||
gopkg.in/src-d/go-billy.v4 v4.3.2
|
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
|
||||||
gopkg.in/yaml.v2 v2.2.7
|
|
||||||
)
|
|
82
go.sum
82
go.sum
@ -1,82 +0,0 @@
|
|||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
|
||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
|
||||||
github.com/bmatcuk/doublestar v1.2.2 h1:oC24CykoSAB8zd7XgruHo33E0cHJf/WhQA/7BeXj+x0=
|
|
||||||
github.com/bmatcuk/doublestar v1.2.2/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
|
||||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
|
||||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
|
||||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
|
||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
|
||||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
|
||||||
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
|
||||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
|
||||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0=
|
|
||||||
golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
|
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
|
||||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
|
||||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
|
|
||||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
|
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
|
||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
104
repo.go
104
repo.go
@ -1,104 +0,0 @@
|
|||||||
// Package dehub TODO needs package docs
|
|
||||||
package dehub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub/fs"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"gopkg.in/src-d/go-billy.v4"
|
|
||||||
"gopkg.in/src-d/go-billy.v4/memfs"
|
|
||||||
"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/memory"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DehubDir defines the name of the directory where all dehub-related files are
|
|
||||||
// expected to be found.
|
|
||||||
DehubDir = ".dehub"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ConfigPath defines the expected path to the Repo's configuration file.
|
|
||||||
ConfigPath = filepath.Join(DehubDir, "config.yml")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Repo is an object which allows accessing and modifying the dehub repo.
|
|
||||||
type Repo struct {
|
|
||||||
GitRepo *git.Repository
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenRepo opens the dehub repo in the given directory and returns the object
|
|
||||||
// for it.
|
|
||||||
//
|
|
||||||
// The given path is expected to have a git repo and .dehub folder already
|
|
||||||
// initialized.
|
|
||||||
func OpenRepo(path string) (*Repo, error) {
|
|
||||||
r := Repo{}
|
|
||||||
var err error
|
|
||||||
openOpts := &git.PlainOpenOptions{
|
|
||||||
DetectDotGit: true,
|
|
||||||
}
|
|
||||||
if r.GitRepo, err = git.PlainOpenWithOptions(path, openOpts); err != nil {
|
|
||||||
return nil, fmt.Errorf("could not open git repo: %w", err)
|
|
||||||
}
|
|
||||||
return &r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitMemRepo initializes an empty repository which only exists in memory.
|
|
||||||
func InitMemRepo() *Repo {
|
|
||||||
r, err := git.Init(memory.NewStorage(), memfs.New())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Repo{GitRepo: r}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) billyFilesystem() (billy.Filesystem, error) {
|
|
||||||
w, err := r.GitRepo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not open git worktree: %w", err)
|
|
||||||
}
|
|
||||||
return w.Filesystem, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) head() (*object.Commit, *object.Tree, error) {
|
|
||||||
head, err := r.GitRepo.Head()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("could not get repo HEAD: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
headHash := head.Hash()
|
|
||||||
headCommit, err := r.GitRepo.CommitObject(headHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("could not get commit at HEAD (%q): %w", headHash, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
headTree, err := r.GitRepo.TreeObject(headCommit.TreeHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("could not get tree object at HEAD (commit:%q tree:%q): %w",
|
|
||||||
headHash, headCommit.TreeHash, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return headCommit, headTree, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// headOrRawFS returns an FS based on the HEAD commit, or if there is no HEAD
|
|
||||||
// commit (it's an empty repo) an FS based on the raw filesystem.
|
|
||||||
func (r *Repo) headOrRawFS() (fs.FS, error) {
|
|
||||||
_, headTree, err := r.head()
|
|
||||||
if errors.Is(err, plumbing.ErrReferenceNotFound) {
|
|
||||||
bfs, err := r.billyFilesystem()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get underlying filesystem: %w", err)
|
|
||||||
}
|
|
||||||
return fs.FromBillyFilesystem(bfs), nil
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get HEAD tree: %w", err)
|
|
||||||
}
|
|
||||||
return fs.FromTree(headTree), nil
|
|
||||||
}
|
|
118
repo_test.go
118
repo_test.go
@ -1,118 +0,0 @@
|
|||||||
package dehub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"dehub/accessctl"
|
|
||||||
"dehub/sigcred"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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.AccessControl{
|
|
||||||
{
|
|
||||||
Pattern: "**",
|
|
||||||
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) commit(msg string) plumbing.Hash {
|
|
||||||
w, err := h.repo.GitRepo.Worktree()
|
|
||||||
if err != nil {
|
|
||||||
h.t.Fatal(err)
|
|
||||||
}
|
|
||||||
hash, err := w.Commit(msg, &git.CommitOptions{
|
|
||||||
Author: &object.Signature{Name: "god"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
package sigcred
|
|
||||||
|
|
||||||
import "dehub/typeobj"
|
|
||||||
|
|
||||||
// Credential represents a credential which has been attached to a commit which
|
|
||||||
// hopefully will allow it to be included in the master branch. Exactly one
|
|
||||||
// field tagged with "type" should be set.
|
|
||||||
type Credential struct {
|
|
||||||
PGPSignature *CredentialPGPSignature `type:"pgp_signature"`
|
|
||||||
|
|
||||||
// AccountID specifies the account which generated this Credential. The
|
|
||||||
// Credentials produced by the Signifier.Sign method do not fill this field
|
|
||||||
// in.
|
|
||||||
AccountID string `yaml:"account"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalYAML implements the yaml.Marshaler interface.
|
|
||||||
func (c Credential) MarshalYAML() (interface{}, error) {
|
|
||||||
return typeobj.MarshalYAML(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
||||||
func (c *Credential) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
return typeobj.UnmarshalYAML(c, unmarshal)
|
|
||||||
}
|
|
279
sigcred/pgp.go
279
sigcred/pgp.go
@ -1,279 +0,0 @@
|
|||||||
package sigcred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/sha256"
|
|
||||||
"dehub/fs"
|
|
||||||
"dehub/yamlutil"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/openpgp/armor"
|
|
||||||
"golang.org/x/crypto/openpgp/packet"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CredentialPGPSignature describes a PGP signature which has been used to sign
|
|
||||||
// a commit.
|
|
||||||
type CredentialPGPSignature struct {
|
|
||||||
PubKeyID string `yaml:"pub_key_id"`
|
|
||||||
Body yamlutil.Blob `yaml:"body"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type pgpPubKey struct {
|
|
||||||
pubKey *packet.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPGPPubKey(r io.Reader) (pgpPubKey, error) {
|
|
||||||
// TODO support non-armored keys as well
|
|
||||||
block, err := armor.Decode(r)
|
|
||||||
if err != nil {
|
|
||||||
return pgpPubKey{}, fmt.Errorf("could not decode armored PGP public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt, err := packet.Read(block.Body)
|
|
||||||
if err != nil {
|
|
||||||
return pgpPubKey{}, fmt.Errorf("could not read PGP public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pubKey, ok := pkt.(*packet.PublicKey)
|
|
||||||
if !ok {
|
|
||||||
return pgpPubKey{}, fmt.Errorf("packet is not a public key, it's a %T", pkt)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pgpPubKey{pubKey: pubKey}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s pgpPubKey) Signed(_ fs.FS, cred Credential) (bool, error) {
|
|
||||||
if cred.PGPSignature == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return cred.PGPSignature.PubKeyID == s.pubKey.KeyIdString(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s pgpPubKey) Verify(_ fs.FS, data []byte, cred Credential) error {
|
|
||||||
credSig := cred.PGPSignature
|
|
||||||
if credSig == nil {
|
|
||||||
return fmt.Errorf("SignifierPGPFile cannot verify %+v", cred)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkt, err := packet.Read(bytes.NewBuffer(credSig.Body))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not read signature packet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sigPkt, ok := pkt.(*packet.Signature)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("signature bytes were parsed as a %T, not a signature", pkt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The gpg process which is invoked during normal signing automatically
|
|
||||||
// hashes whatever is piped to it. The VerifySignature method in the openpgp
|
|
||||||
// package expects you to do it yourself.
|
|
||||||
h := sigPkt.Hash.New()
|
|
||||||
h.Write(data)
|
|
||||||
return s.pubKey.VerifySignature(h, sigPkt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s pgpPubKey) encode() ([]byte, error) {
|
|
||||||
body := new(bytes.Buffer)
|
|
||||||
armorEncoder, err := armor.Encode(body, "PGP PUBLIC KEY", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error initializing armor encoder: %w", err)
|
|
||||||
} else if err := s.pubKey.Serialize(armorEncoder); err != nil {
|
|
||||||
return nil, fmt.Errorf("error encoding public key: %w", err)
|
|
||||||
} else if err := armorEncoder.Close(); err != nil {
|
|
||||||
return nil, fmt.Errorf("error closing armor encoder: %w", err)
|
|
||||||
}
|
|
||||||
return body.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s pgpPubKey) asSignfier() (SignifierPGP, error) {
|
|
||||||
body, err := s.encode()
|
|
||||||
if err != nil {
|
|
||||||
return SignifierPGP{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return SignifierPGP{
|
|
||||||
Body: string(body),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type pgpPrivKey struct {
|
|
||||||
pgpPubKey
|
|
||||||
privKey *packet.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignifierPGPTmp returns a direct implementation of the SignifierInterface
|
|
||||||
// which uses a random private key generated in memory, as well as an armored
|
|
||||||
// version of its public key.
|
|
||||||
func SignifierPGPTmp(randReader io.Reader) (SignifierInterface, []byte) {
|
|
||||||
rawPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), randReader)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
privKeyRaw := packet.NewECDSAPrivateKey(time.Now(), rawPrivKey)
|
|
||||||
privKey := pgpPrivKey{
|
|
||||||
pgpPubKey: pgpPubKey{
|
|
||||||
pubKey: &privKeyRaw.PublicKey,
|
|
||||||
},
|
|
||||||
privKey: privKeyRaw,
|
|
||||||
}
|
|
||||||
|
|
||||||
pubKeyBody, err := privKey.pgpPubKey.encode()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return privKey, pubKeyBody
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s pgpPrivKey) Sign(_ fs.FS, data []byte) (Credential, error) {
|
|
||||||
h := sha256.New()
|
|
||||||
h.Write(data)
|
|
||||||
var sig packet.Signature
|
|
||||||
sig.Hash = crypto.SHA256
|
|
||||||
sig.PubKeyAlgo = s.pubKey.PubKeyAlgo
|
|
||||||
if err := sig.Sign(h, s.privKey, nil); err != nil {
|
|
||||||
return Credential{}, fmt.Errorf("failed to sign data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := new(bytes.Buffer)
|
|
||||||
if err := sig.Serialize(body); err != nil {
|
|
||||||
return Credential{}, fmt.Errorf("failed to serialize signature: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Credential{
|
|
||||||
PGPSignature: &CredentialPGPSignature{
|
|
||||||
PubKeyID: s.pubKey.KeyIdString(),
|
|
||||||
Body: body.Bytes(),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignifierPGP describes a pgp public key whose corresponding private key will
|
|
||||||
// be used as a signing key.
|
|
||||||
type SignifierPGP struct {
|
|
||||||
Body string `yaml:"body"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ SignifierInterface = SignifierPGP{}
|
|
||||||
|
|
||||||
func (s SignifierPGP) load() (pgpPubKey, error) {
|
|
||||||
return newPGPPubKey(strings.NewReader(s.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign will sign the given arbitrary bytes using the private key corresponding
|
|
||||||
// to the pgp public key embedded in this Signifier.
|
|
||||||
func (s SignifierPGP) Sign(fs fs.FS, data []byte) (Credential, error) {
|
|
||||||
sigPGP, err := s.load()
|
|
||||||
if err != nil {
|
|
||||||
return Credential{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr := new(bytes.Buffer)
|
|
||||||
cmd := exec.Command("gpg",
|
|
||||||
"--openpgp",
|
|
||||||
"--detach-sign",
|
|
||||||
"--local-user", sigPGP.pubKey.KeyIdString())
|
|
||||||
cmd.Stdin = bytes.NewBuffer(data)
|
|
||||||
cmd.Stderr = stderr
|
|
||||||
sig, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return Credential{}, fmt.Errorf("error signing with gpg (%v): %s", err, stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return Credential{
|
|
||||||
PGPSignature: &CredentialPGPSignature{
|
|
||||||
PubKeyID: sigPGP.pubKey.KeyIdString(),
|
|
||||||
Body: sig,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signed returns true if the private key corresponding to the pgp public key
|
|
||||||
// embedded in this Signifier was used to produce the given Credential.
|
|
||||||
func (s SignifierPGP) Signed(fs fs.FS, cred Credential) (bool, error) {
|
|
||||||
sigPGP, err := s.load()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return sigPGP.Signed(fs, cred)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify asserts that the given signature was produced by this key signing the
|
|
||||||
// given piece of data.
|
|
||||||
func (s SignifierPGP) Verify(fs fs.FS, data []byte, cred Credential) error {
|
|
||||||
sigPGP, err := s.load()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sigPGP.Verify(fs, data, cred)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignifierPGPFile is the same as SignifierPGP, except that the public key is
|
|
||||||
// found in the repo rather than encoded into the object.
|
|
||||||
type SignifierPGPFile struct {
|
|
||||||
Path string `yaml:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ SignifierInterface = SignifierPGPFile{}
|
|
||||||
|
|
||||||
func (s SignifierPGPFile) load(fs fs.FS) (SignifierPGP, error) {
|
|
||||||
path := filepath.Clean(s.Path)
|
|
||||||
fr, err := fs.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return SignifierPGP{}, fmt.Errorf("could not open PGP public key file at %q: %w", path, err)
|
|
||||||
}
|
|
||||||
defer fr.Close()
|
|
||||||
|
|
||||||
pubKeyB, err := ioutil.ReadAll(fr)
|
|
||||||
if err != nil {
|
|
||||||
return SignifierPGP{}, fmt.Errorf("could not read PGP public key from file blob at %q: %w", s.Path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return SignifierPGP{Body: string(pubKeyB)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign will sign the given arbitrary bytes using the private key corresponding
|
|
||||||
// to the pgp public key located by this Signifier.
|
|
||||||
func (s SignifierPGPFile) Sign(fs fs.FS, data []byte) (Credential, error) {
|
|
||||||
sigPGP, err := s.load(fs)
|
|
||||||
if err != nil {
|
|
||||||
return Credential{}, err
|
|
||||||
}
|
|
||||||
return sigPGP.Sign(fs, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signed returns true if the private key corresponding to the pgp public key
|
|
||||||
// located by this Signifier was used to produce the given Credential.
|
|
||||||
func (s SignifierPGPFile) Signed(fs fs.FS, cred Credential) (bool, error) {
|
|
||||||
if cred.PGPSignature == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sigPGP, err := s.load(fs)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return sigPGP.Signed(fs, cred)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify asserts that the given signature was produced by this key signing the
|
|
||||||
// given piece of data.
|
|
||||||
func (s SignifierPGPFile) Verify(fs fs.FS, data []byte, cred Credential) error {
|
|
||||||
sigPGP, err := s.load(fs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return sigPGP.Verify(fs, data, cred)
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package sigcred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub/fs"
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// There are not currently tests for testing pgp signature creation, as they
|
|
||||||
// require calls out to the gpg executable. Wrapping tests in docker containers
|
|
||||||
// would make this doable.
|
|
||||||
|
|
||||||
func TestPGPVerification(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
descr string
|
|
||||||
init func(pubKeyBody []byte) (SignifierInterface, fs.FS)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
descr: "SignifierPGP",
|
|
||||||
init: func(pubKeyBody []byte) (SignifierInterface, fs.FS) {
|
|
||||||
return SignifierPGP{Body: string(pubKeyBody)}, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "SignifierPGPFile",
|
|
||||||
init: func(pubKeyBody []byte) (SignifierInterface, fs.FS) {
|
|
||||||
pubKeyPath := "some/dir/pubkey.asc"
|
|
||||||
fs := fs.Stub{pubKeyPath: pubKeyBody}
|
|
||||||
sigPGPFile := SignifierPGPFile{Path: pubKeyPath}
|
|
||||||
return sigPGPFile, fs
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.descr, func(t *testing.T) {
|
|
||||||
seed := time.Now().UnixNano()
|
|
||||||
t.Logf("seed: %d", seed)
|
|
||||||
rand := rand.New(rand.NewSource(seed))
|
|
||||||
privKey, pubKeyBody := SignifierPGPTmp(rand)
|
|
||||||
|
|
||||||
sig, fs := test.init(pubKeyBody)
|
|
||||||
data := make([]byte, rand.Intn(1024))
|
|
||||||
if _, err := rand.Read(data); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cred, err := privKey.Sign(nil, data)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signed, err := sig.Signed(fs, cred)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if !signed {
|
|
||||||
t.Fatal("expected signed to be true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sig.Verify(fs, data, cred); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
// Package sigcred implements the Signifier and Credential types, which
|
|
||||||
// interplay together to provide the ability to sign arbitrary blobs of data
|
|
||||||
// (producing Credentials) and to verify those Credentials within the context of
|
|
||||||
// a dehub repo.
|
|
||||||
package sigcred
|
|
@ -1,50 +0,0 @@
|
|||||||
package sigcred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"dehub/fs"
|
|
||||||
"dehub/typeobj"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Signifier reprsents a single signing method being defined in the Config. Only
|
|
||||||
// one field should be set on each Signifier.
|
|
||||||
type Signifier struct {
|
|
||||||
PGPPublicKey *SignifierPGP `type:"pgp_public_key"`
|
|
||||||
PGPPublicKeyFile *SignifierPGPFile `type:"pgp_public_key_file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalYAML implements the yaml.Marshaler interface.
|
|
||||||
func (s Signifier) MarshalYAML() (interface{}, error) {
|
|
||||||
return typeobj.MarshalYAML(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
||||||
func (s *Signifier) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
return typeobj.UnmarshalYAML(s, unmarshal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface returns the SignifierInterface instance encapsulated by this
|
|
||||||
// Signifier object.
|
|
||||||
func (s Signifier) Interface() (SignifierInterface, error) {
|
|
||||||
el, _, err := typeobj.Element(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return el.(SignifierInterface), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignifierInterface describes the methods that all Signifiers must implement.
|
|
||||||
type SignifierInterface interface {
|
|
||||||
// Sign returns a Credential containing a signature of the given data.
|
|
||||||
//
|
|
||||||
// tree can be used to find the Signifier at a particular snapshot.
|
|
||||||
Sign(fs fs.FS, data []byte) (Credential, error)
|
|
||||||
|
|
||||||
// Signed returns true if the Signifier was used to sign the Credential.
|
|
||||||
Signed(fs fs.FS, cred Credential) (bool, error)
|
|
||||||
|
|
||||||
// Verify asserts that the Signifier produced the given Credential for the
|
|
||||||
// given data set, or returns an error.
|
|
||||||
//
|
|
||||||
// tree can be used to find the Signifier at a particular snapshot.
|
|
||||||
Verify(fs fs.FS, data []byte, cred Credential) error
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
// Package typeobj implements a set of utility functions intended to be used on
|
|
||||||
// union structs whose fields are tagged with the "type" tag and which expect
|
|
||||||
// only one of the fields to be set. For example:
|
|
||||||
//
|
|
||||||
// type OuterType struct {
|
|
||||||
// A *InnerTypeA `type:"a"`
|
|
||||||
// B *InnerTypeB `type:"b"`
|
|
||||||
// C *InnerTypeC `type:"c"`
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
package typeobj
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UnmarshalYAML is intended to be used within the UnmarshalYAML method of a
|
|
||||||
// union struct. It will use the given input data's "type" field and match that
|
|
||||||
// to the struct field tagged with that value. it will then unmarshal the input
|
|
||||||
// data into that inner field.
|
|
||||||
func UnmarshalYAML(i interface{}, unmarshal func(interface{}) error) error {
|
|
||||||
val := reflect.Indirect(reflect.ValueOf(i))
|
|
||||||
if !val.CanSet() {
|
|
||||||
return fmt.Errorf("cannot unmarshal into value of type %T", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshal in all non-typeobj fields. construct a type which wraps the
|
|
||||||
// given one, hiding its UnmarshalYAML method (if it has one), and unmarshal
|
|
||||||
// onto that directly. The "type" field is also unmarshaled at this stage.
|
|
||||||
valWrap := reflect.New(reflect.StructOf([]reflect.StructField{
|
|
||||||
reflect.StructField{
|
|
||||||
Name: "Type",
|
|
||||||
Type: typeOfString,
|
|
||||||
Tag: `yaml:"type"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Val",
|
|
||||||
Type: val.Type(),
|
|
||||||
Tag: `yaml:",inline"`,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
if err := unmarshal(valWrap.Interface()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
typeVal := valWrap.Elem().Field(0).String()
|
|
||||||
val.Set(valWrap.Elem().Field(1))
|
|
||||||
|
|
||||||
typ := val.Type()
|
|
||||||
for i := 0; i < val.NumField(); i++ {
|
|
||||||
fieldVal, fieldTyp := val.Field(i), typ.Field(i)
|
|
||||||
if fieldTyp.Tag.Get("type") != typeVal {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var valInto interface{}
|
|
||||||
if fieldVal.Kind() == reflect.Ptr {
|
|
||||||
newFieldVal := reflect.New(fieldTyp.Type.Elem())
|
|
||||||
fieldVal.Set(newFieldVal)
|
|
||||||
valInto = newFieldVal.Interface()
|
|
||||||
} else {
|
|
||||||
valInto = fieldVal.Addr().Interface()
|
|
||||||
}
|
|
||||||
return unmarshal(valInto)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("invalid type value %q", typeVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
// val should be of kind struct
|
|
||||||
func element(val reflect.Value) (reflect.Value, string, []int, error) {
|
|
||||||
typ := val.Type()
|
|
||||||
numFields := val.NumField()
|
|
||||||
|
|
||||||
var fieldVal reflect.Value
|
|
||||||
var typeTag string
|
|
||||||
nonTypeFields := make([]int, 0, numFields)
|
|
||||||
for i := 0; i < numFields; i++ {
|
|
||||||
innerFieldVal := val.Field(i)
|
|
||||||
innerTypeTag := typ.Field(i).Tag.Get("type")
|
|
||||||
if innerTypeTag == "" {
|
|
||||||
nonTypeFields = append(nonTypeFields, i)
|
|
||||||
} else if innerFieldVal.IsZero() {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
fieldVal = innerFieldVal
|
|
||||||
typeTag = innerTypeTag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fieldVal.IsZero() {
|
|
||||||
return reflect.Value{}, "", nil, errors.New(`no non-zero fields tagged with "type"`)
|
|
||||||
}
|
|
||||||
return fieldVal, typeTag, nonTypeFields, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Element returns the value of the first non-zero field tagged with "type", as
|
|
||||||
// well as the value of the "type" tag.
|
|
||||||
func Element(i interface{}) (interface{}, string, error) {
|
|
||||||
val := reflect.Indirect(reflect.ValueOf(i))
|
|
||||||
fieldVal, tag, _, err := element(val)
|
|
||||||
if err != nil {
|
|
||||||
return fieldVal, tag, err
|
|
||||||
}
|
|
||||||
return fieldVal.Interface(), tag, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var typeOfString = reflect.TypeOf("string")
|
|
||||||
|
|
||||||
// MarshalYAML is intended to be used within the MarshalYAML method of a union
|
|
||||||
// struct. It will find the first field of the given struct which has a "type"
|
|
||||||
// tag and is non-zero. It will then marshal that field's value, inlining an
|
|
||||||
// extra YAML field "type" whose value is the value of the "type" tag on the
|
|
||||||
// struct field, and return that.
|
|
||||||
func MarshalYAML(i interface{}) (interface{}, error) {
|
|
||||||
val := reflect.Indirect(reflect.ValueOf(i))
|
|
||||||
typ := val.Type()
|
|
||||||
fieldVal, typeTag, nonTypeFields, err := element(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldVal = reflect.Indirect(fieldVal)
|
|
||||||
if fieldVal.Kind() != reflect.Struct {
|
|
||||||
return nil, fmt.Errorf("cannot marshal non-struct type %T", fieldVal.Interface())
|
|
||||||
}
|
|
||||||
|
|
||||||
structFields := make([]reflect.StructField, 0, len(nonTypeFields)+2)
|
|
||||||
structFields = append(structFields,
|
|
||||||
reflect.StructField{
|
|
||||||
Name: "Type",
|
|
||||||
Type: typeOfString,
|
|
||||||
Tag: `yaml:"type"`,
|
|
||||||
},
|
|
||||||
reflect.StructField{
|
|
||||||
Name: "Val",
|
|
||||||
Type: fieldVal.Type(),
|
|
||||||
Tag: `yaml:",inline"`,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
nonTypeFieldVals := make([]reflect.Value, len(nonTypeFields))
|
|
||||||
for i, fieldIndex := range nonTypeFields {
|
|
||||||
fieldVal, fieldType := val.Field(fieldIndex), typ.Field(fieldIndex)
|
|
||||||
structFields = append(structFields, fieldType)
|
|
||||||
nonTypeFieldVals[i] = fieldVal
|
|
||||||
}
|
|
||||||
|
|
||||||
outVal := reflect.New(reflect.StructOf(structFields))
|
|
||||||
outVal.Elem().Field(0).Set(reflect.ValueOf(typeTag))
|
|
||||||
outVal.Elem().Field(1).Set(fieldVal)
|
|
||||||
for i, fieldVal := range nonTypeFieldVals {
|
|
||||||
outVal.Elem().Field(2 + i).Set(fieldVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return outVal.Interface(), nil
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
package typeobj
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type foo struct {
|
|
||||||
A int `yaml:"a"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type bar struct {
|
|
||||||
B int `yaml:"b"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type outer struct {
|
|
||||||
Foo foo `type:"foo"`
|
|
||||||
Bar *bar `type:"bar"`
|
|
||||||
|
|
||||||
Other string `yaml:"other_field,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o outer) MarshalYAML() (interface{}, error) {
|
|
||||||
return MarshalYAML(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *outer) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
return UnmarshalYAML(o, unmarshal)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTypeObj(t *testing.T) {
|
|
||||||
|
|
||||||
type test struct {
|
|
||||||
descr string
|
|
||||||
str string
|
|
||||||
err bool
|
|
||||||
other string
|
|
||||||
|
|
||||||
obj outer
|
|
||||||
typeTag string
|
|
||||||
elem interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []test{
|
|
||||||
{
|
|
||||||
descr: "no type set",
|
|
||||||
str: `{}`,
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "unknown type set",
|
|
||||||
str: "type: baz",
|
|
||||||
err: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "foo set",
|
|
||||||
str: "type: foo\na: 1\n",
|
|
||||||
obj: outer{Foo: foo{A: 1}},
|
|
||||||
typeTag: "foo",
|
|
||||||
elem: foo{A: 1},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "bar set",
|
|
||||||
str: "type: bar\nb: 1\n",
|
|
||||||
obj: outer{Bar: &bar{B: 1}},
|
|
||||||
typeTag: "bar",
|
|
||||||
elem: &bar{B: 1},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "foo and other_field set",
|
|
||||||
str: "type: foo\na: 1\nother_field: aaa\n",
|
|
||||||
obj: outer{Foo: foo{A: 1}, Other: "aaa"},
|
|
||||||
typeTag: "foo",
|
|
||||||
elem: foo{A: 1},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.descr, func(t *testing.T) {
|
|
||||||
var o outer
|
|
||||||
err := yaml.Unmarshal([]byte(test.str), &o)
|
|
||||||
if test.err && err != nil {
|
|
||||||
return
|
|
||||||
} else if test.err && err == nil {
|
|
||||||
t.Fatal("expected error when unmarshaling but there was none")
|
|
||||||
} else if !test.err && err != nil {
|
|
||||||
t.Fatalf("unmarshaling %q returned unexpected error: %v", test.str, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(o, test.obj) {
|
|
||||||
t.Fatalf("test expected value:\n%s\nbut got value:\n%s", spew.Sprint(test.obj), spew.Sprint(o))
|
|
||||||
}
|
|
||||||
|
|
||||||
elem, typeTag, err := Element(o)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error when calling Element(%s): %v", spew.Sprint(o), err)
|
|
||||||
} else if !reflect.DeepEqual(elem, test.elem) {
|
|
||||||
t.Fatalf("test expected elem value:\n%s\nbut got value:\n%s", spew.Sprint(test.elem), spew.Sprint(elem))
|
|
||||||
} else if typeTag != test.typeTag {
|
|
||||||
t.Fatalf("test expected typeTag value %q but got %q", test.typeTag, typeTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := yaml.Marshal(o)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error marshaling %s: %v", spew.Sprint(o), err)
|
|
||||||
} else if test.str != string(b) {
|
|
||||||
t.Fatalf("test expected to marshal to %q, but instead marshaled to %q", test.str, b)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
// Package yamlutil contains utility types which are useful for dealing with the
|
|
||||||
// yaml package.
|
|
||||||
package yamlutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Blob encodes and decodes a byte slice as a standard base-64 encoded yaml
|
|
||||||
// string.
|
|
||||||
type Blob []byte
|
|
||||||
|
|
||||||
// MarshalYAML implements the yaml.Marshaler interface.
|
|
||||||
func (b Blob) MarshalYAML() (interface{}, error) {
|
|
||||||
return base64.StdEncoding.EncodeToString([]byte(b)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
|
||||||
func (b *Blob) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
||||||
var b64 string
|
|
||||||
if err := unmarshal(&b64); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b64Dec, err := base64.StdEncoding.DecodeString(b64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*b = b64Dec
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
package yamlutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBlob(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
descr string
|
|
||||||
in Blob
|
|
||||||
exp string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
descr: "empty",
|
|
||||||
in: Blob(""),
|
|
||||||
exp: `""`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "zero",
|
|
||||||
in: Blob{0},
|
|
||||||
exp: "AA==",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "zeros",
|
|
||||||
in: Blob{0, 0, 0},
|
|
||||||
exp: "AAAA",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
descr: "foo",
|
|
||||||
in: Blob("foo"),
|
|
||||||
exp: "Zm9v",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.descr, func(t *testing.T) {
|
|
||||||
out, err := yaml.Marshal(test.in)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error marshaling %q: %v", test.in, err)
|
|
||||||
} else if test.exp+"\n" != string(out) {
|
|
||||||
t.Fatalf("marshal exp:%q got:%q", test.exp+"\n", out)
|
|
||||||
}
|
|
||||||
|
|
||||||
var blob Blob
|
|
||||||
if err := yaml.Unmarshal(out, &blob); err != nil {
|
|
||||||
t.Fatalf("error unmarshaling %q: %v", out, err)
|
|
||||||
} else if !bytes.Equal([]byte(blob), []byte(test.in)) {
|
|
||||||
t.Fatalf("unmarshal exp:%q got:%q", test.in, blob)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user