Compare commits
2 Commits
main
...
public/wel
Author | SHA1 | Date |
---|---|---|
![]() |
1147264ff1 | 4 years ago |
![]() |
f5584f1505 | 4 years ago |
@ -1 +1 @@ |
||||
dehub |
||||
/dehub |
||||
|
@ -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. |
||||
|
@ -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() |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
||||