diff options
Diffstat (limited to 'identity')
-rw-r--r-- | identity/identity.go | 162 | ||||
-rw-r--r-- | identity/identity_actions.go | 43 | ||||
-rw-r--r-- | identity/identity_test.go | 119 | ||||
-rw-r--r-- | identity/version.go | 78 |
4 files changed, 218 insertions, 184 deletions
diff --git a/identity/identity.go b/identity/identity.go index 3877e346..59973489 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -21,17 +21,22 @@ const identityConfigKey = "git-bug.identity" var _ Interface = &Identity{} type Identity struct { - id string - Versions []*Version + // Id used as unique identifier + id string + + lastCommit git.Hash + + // all the successive version of the identity + versions []*Version } func NewIdentity(name string, email string) *Identity { return &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: name, - Email: email, - Nonce: makeNonce(20), + name: name, + email: email, + nonce: makeNonce(20), }, }, } @@ -39,13 +44,13 @@ func NewIdentity(name string, email string) *Identity { func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity { return &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: name, - Email: email, - Login: login, - AvatarUrl: avatarUrl, - Nonce: makeNonce(20), + name: name, + email: email, + login: login, + avatarURL: avatarUrl, + nonce: makeNonce(20), }, }, } @@ -84,13 +89,15 @@ func read(repo repository.Repo, ref string) (*Identity, error) { hashes, err := repo.ListCommits(ref) - var versions []*Version - // TODO: this is not perfect, it might be a command invoke error if err != nil { return nil, ErrIdentityNotExist } + i := &Identity{ + id: id, + } + for _, hash := range hashes { entries, err := repo.ListEntries(hash) if err != nil { @@ -121,14 +128,12 @@ func read(repo repository.Repo, ref string) (*Identity, error) { // tag the version with the commit hash version.commitHash = hash + i.lastCommit = hash - versions = append(versions, &version) + i.versions = append(i.versions, &version) } - return &Identity{ - id: id, - Versions: versions, - }, nil + return i, nil } // NewFromGitUser will query the repository for user detail and @@ -182,7 +187,7 @@ func GetUserIdentity(repo repository.Repo) (*Identity, error) { } func (i *Identity) AddVersion(version *Version) { - i.Versions = append(i.Versions, version) + i.versions = append(i.versions, version) } // Write the identity into the Repository. In particular, this ensure that @@ -190,11 +195,9 @@ func (i *Identity) AddVersion(version *Version) { func (i *Identity) Commit(repo repository.Repo) error { // Todo: check for mismatch between memory and commited data - var lastCommit git.Hash = "" - - for _, v := range i.Versions { + for _, v := range i.versions { if v.commitHash != "" { - lastCommit = v.commitHash + i.lastCommit = v.commitHash // ignore already commited versions continue } @@ -215,8 +218,8 @@ func (i *Identity) Commit(repo repository.Repo) error { } var commitHash git.Hash - if lastCommit != "" { - commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit) + if i.lastCommit != "" { + commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit) } else { commitHash, err = repo.StoreCommit(treeHash) } @@ -225,7 +228,8 @@ func (i *Identity) Commit(repo repository.Repo) error { return err } - lastCommit = commitHash + i.lastCommit = commitHash + v.commitHash = commitHash // if it was the first commit, use the commit hash as the Identity id if i.id == "" { @@ -238,7 +242,7 @@ func (i *Identity) Commit(repo repository.Repo) error { } ref := fmt.Sprintf("%s%s", identityRefPattern, i.id) - err := repo.UpdateRef(ref, lastCommit) + err := repo.UpdateRef(ref, i.lastCommit) if err != nil { return err @@ -247,31 +251,93 @@ func (i *Identity) Commit(repo repository.Repo) error { return nil } +// Merge will merge a different version of the same Identity +// +// To make sure that an Identity history can't be altered, a strict fast-forward +// only policy is applied here. As an Identity should be tied to a single user, this +// should work in practice but it does leave a possibility that a user would edit his +// Identity from two different repo concurrently and push the changes in a non-centralized +// network of repositories. In this case, it would result in some of the repo accepting one +// version and some other accepting another, preventing the network in general to converge +// to the same result. This would create a sort of partition of the network, and manual +// cleaning would be required. +// +// An alternative approach would be to have a determinist rebase: +// - any commits present in both local and remote version would be kept, never changed. +// - newer commits would be merged in a linear chain of commits, ordered based on the +// Lamport time +// +// However, this approach leave the possibility, in the case of a compromised crypto keys, +// of forging a new version with a bogus Lamport time to be inserted before a legit version, +// invalidating the correct version and hijacking the Identity. There would only be a short +// period of time where this would be possible (before the network converge) but I'm not +// confident enough to implement that. I choose the strict fast-forward only approach, +// despite it's potential problem with two different version as mentioned above. +func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) { + if i.id != other.id { + return false, errors.New("merging unrelated identities is not supported") + } + + if i.lastCommit == "" || other.lastCommit == "" { + return false, errors.New("can't merge identities that has never been stored") + } + + /*ancestor, err := repo.FindCommonAncestor(i.lastCommit, other.lastCommit) + if err != nil { + return false, errors.Wrap(err, "can't find common ancestor") + }*/ + + modified := false + for j, otherVersion := range other.versions { + // if there is more version in other, take them + if len(i.versions) == j { + i.versions = append(i.versions, otherVersion) + i.lastCommit = otherVersion.commitHash + modified = true + } + + // we have a non fast-forward merge. + // as explained in the doc above, refusing to merge + if i.versions[j].commitHash != otherVersion.commitHash { + return false, errors.New("non fast-forward identity merge") + } + } + + if modified { + err := repo.UpdateRef(identityRefPattern+i.id, i.lastCommit) + if err != nil { + return false, err + } + } + + return false, nil +} + // Validate check if the Identity data is valid func (i *Identity) Validate() error { lastTime := lamport.Time(0) - for _, v := range i.Versions { + for _, v := range i.versions { if err := v.Validate(); err != nil { return err } - if v.Time < lastTime { - return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.Time) + if v.time < lastTime { + return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time) } - lastTime = v.Time + lastTime = v.time } return nil } func (i *Identity) lastVersion() *Version { - if len(i.Versions) <= 0 { + if len(i.versions) <= 0 { panic("no version at all") } - return i.Versions[len(i.Versions)-1] + return i.versions[len(i.versions)-1] } // Id return the Identity identifier @@ -286,27 +352,27 @@ func (i *Identity) Id() string { // Name return the last version of the name func (i *Identity) Name() string { - return i.lastVersion().Name + return i.lastVersion().name } // Email return the last version of the email func (i *Identity) Email() string { - return i.lastVersion().Email + return i.lastVersion().email } // Login return the last version of the login func (i *Identity) Login() string { - return i.lastVersion().Login + return i.lastVersion().login } -// Login return the last version of the Avatar URL +// AvatarUrl return the last version of the Avatar URL func (i *Identity) AvatarUrl() string { - return i.lastVersion().AvatarUrl + return i.lastVersion().avatarURL } -// Login return the last version of the valid keys +// Keys return the last version of the valid keys func (i *Identity) Keys() []Key { - return i.lastVersion().Keys + return i.lastVersion().keys } // IsProtected return true if the chain of git commits started to be signed. @@ -320,12 +386,12 @@ func (i *Identity) IsProtected() bool { func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key { var result []Key - for _, v := range i.Versions { - if v.Time > time { + for _, v := range i.versions { + if v.time > time { return result } - result = v.Keys + result = v.keys } return result @@ -357,8 +423,8 @@ func (i *Identity) SetMetadata(key string, value string) { func (i *Identity) ImmutableMetadata() map[string]string { metadata := make(map[string]string) - for _, version := range i.Versions { - for key, value := range version.Metadata { + for _, version := range i.versions { + for key, value := range version.metadata { if _, has := metadata[key]; !has { metadata[key] = value } @@ -373,8 +439,8 @@ func (i *Identity) ImmutableMetadata() map[string]string { func (i *Identity) MutableMetadata() map[string]string { metadata := make(map[string]string) - for _, version := range i.Versions { - for key, value := range version.Metadata { + for _, version := range i.versions { + for key, value := range version.metadata { metadata[key] = value } } diff --git a/identity/identity_actions.go b/identity/identity_actions.go index 69f77a2b..da7a064c 100644 --- a/identity/identity_actions.go +++ b/identity/identity_actions.go @@ -46,26 +46,6 @@ func Pull(repo repository.ClockedRepo, remote string) error { } // MergeAll will merge all the available remote identity -// To make sure that an Identity history can't be altered, a strict fast-forward -// only policy is applied here. As an Identity should be tied to a single user, this -// should work in practice but it does leave a possibility that a user would edit his -// Identity from two different repo concurrently and push the changes in a non-centralized -// network of repositories. In this case, it would result some of the repo accepting one -// version, some other accepting another, preventing the network in general to converge -// to the same result. This would create a sort of partition of the network, and manual -// cleaning would be required. -// -// An alternative approach would be to have a determinist rebase: -// - any commits present in both local and remote version would be kept, never changed. -// - newer commits would be merged in a linear chain of commits, ordered based on the -// Lamport time -// -// However, this approach leave the possibility, in the case of a compromised crypto keys, -// of forging a new version with a bogus Lamport time to be inserted before a legit version, -// invalidating the correct version and hijacking the Identity. There would only be a short -// period of time where this would be possible (before the network converge) but I'm not -// confident enough to implement that. I choose the strict fast-forward only approach, -// despite it's potential problem with two different version as mentioned above. func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { out := make(chan MergeResult) @@ -85,20 +65,19 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { id := refSplitted[len(refSplitted)-1] remoteIdentity, err := ReadLocal(repo, remoteRef) - remoteBug, err := readBug(repo, remoteRef) if err != nil { - out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is not readable").Error()) + out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is not readable").Error()) continue } // Check for error in remote data - if err := remoteBug.Validate(); err != nil { - out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error()) + if err := remoteIdentity.Validate(); err != nil { + out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is invalid").Error()) continue } - localRef := bugsRefPattern + remoteBug.Id() + localRef := identityRefPattern + remoteIdentity.Id() localExist, err := repo.RefExist(localRef) if err != nil { @@ -106,7 +85,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { continue } - // the bug is not local yet, simply create the reference + // the identity is not local yet, simply create the reference if !localExist { err := repo.CopyRef(remoteRef, localRef) @@ -115,18 +94,18 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { return } - out <- newMergeStatus(MergeStatusNew, id, remoteBug) + out <- newMergeStatus(MergeStatusNew, id, remoteIdentity) continue } - localBug, err := readBug(repo, localRef) + localIdentity, err := read(repo, localRef) if err != nil { - out <- newMergeError(errors.Wrap(err, "local bug is not readable"), id) + out <- newMergeError(errors.Wrap(err, "local identity is not readable"), id) return } - updated, err := localBug.Merge(repo, remoteBug) + updated, err := localIdentity.Merge(repo, remoteIdentity) if err != nil { out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) @@ -134,9 +113,9 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult { } if updated { - out <- newMergeStatus(MergeStatusUpdated, id, localBug) + out <- newMergeStatus(MergeStatusUpdated, id, localIdentity) } else { - out <- newMergeStatus(MergeStatusNothing, id, localBug) + out <- newMergeStatus(MergeStatusNothing, id, localIdentity) } } }() diff --git a/identity/identity_test.go b/identity/identity_test.go index 3ab49d76..2ddb64ea 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -6,7 +6,6 @@ import ( "github.com/MichaelMure/git-bug/repository" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // Test the commit and load of an Identity with multiple versions @@ -16,10 +15,10 @@ func TestIdentityCommitLoad(t *testing.T) { // single version identity := &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: "René Descartes", - Email: "rene.descartes@example.com", + name: "René Descartes", + email: "rene.descartes@example.com", }, }, } @@ -32,33 +31,33 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err := ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, identity, loaded) + assert.Equal(t, identity, loaded) // multiple version identity = &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Time: 100, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 100, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyA"}, }, }, { - Time: 200, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 200, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyB"}, }, }, { - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyC"}, }, }, @@ -73,24 +72,24 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err = ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, identity, loaded) + assert.Equal(t, identity, loaded) // add more version identity.AddVersion(&Version{ - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyD"}, }, }) identity.AddVersion(&Version{ - Time: 300, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 300, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyE"}, }, }) @@ -103,66 +102,56 @@ func TestIdentityCommitLoad(t *testing.T) { loaded, err = ReadLocal(mockRepo, identity.id) assert.Nil(t, err) commitsAreSet(t, loaded) - equivalentIdentity(t, identity, loaded) + assert.Equal(t, identity, loaded) } func commitsAreSet(t *testing.T, identity *Identity) { - for _, version := range identity.Versions { + for _, version := range identity.versions { assert.NotEmpty(t, version.commitHash) } } -func equivalentIdentity(t *testing.T, expected, actual *Identity) { - require.Equal(t, len(expected.Versions), len(actual.Versions)) - - for i, version := range expected.Versions { - actual.Versions[i].commitHash = version.commitHash - } - - assert.Equal(t, expected, actual) -} - // Test that the correct crypto keys are returned for a given lamport time func TestIdentity_ValidKeysAtTime(t *testing.T) { identity := Identity{ - Versions: []*Version{ + versions: []*Version{ { - Time: 100, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 100, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyA"}, }, }, { - Time: 200, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 200, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyB"}, }, }, { - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyC"}, }, }, { - Time: 201, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 201, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyD"}, }, }, { - Time: 300, - Name: "René Descartes", - Email: "rene.descartes@example.com", - Keys: []Key{ + time: 300, + name: "René Descartes", + email: "rene.descartes@example.com", + keys: []Key{ {PubKey: "pubkeyE"}, }, }, @@ -197,8 +186,8 @@ func TestMetadata(t *testing.T) { // try override identity.AddVersion(&Version{ - Name: "René Descartes", - Email: "rene.descartes@example.com", + name: "René Descartes", + email: "rene.descartes@example.com", }) identity.SetMetadata("key1", "value2") @@ -226,10 +215,10 @@ func TestJSON(t *testing.T) { mockRepo := repository.NewMockRepoForTest() identity := &Identity{ - Versions: []*Version{ + versions: []*Version{ { - Name: "René Descartes", - Email: "rene.descartes@example.com", + name: "René Descartes", + email: "rene.descartes@example.com", }, }, } diff --git a/identity/version.go b/identity/version.go index f8b9cc73..90bf83f2 100644 --- a/identity/version.go +++ b/identity/version.go @@ -24,25 +24,25 @@ type Version struct { // The lamport time at which this version become effective // The reference time is the bug edition lamport clock - Time lamport.Time + time lamport.Time - Name string - Email string - Login string - AvatarUrl string + name string + email string + login string + avatarURL string // The set of keys valid at that time, from this version onward, until they get removed // in a new version. This allow to have multiple key for the same identity (e.g. one per // device) as well as revoke key. - Keys []Key + keys []Key // This optional array is here to ensure a better randomness of the identity id to avoid collisions. // It has no functional purpose and should be ignored. // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys. - Nonce []byte + nonce []byte // A set of arbitrary key/value to store metadata about a version or about an Identity in general. - Metadata map[string]string + metadata map[string]string } type VersionJSON struct { @@ -62,14 +62,14 @@ type VersionJSON struct { func (v *Version) MarshalJSON() ([]byte, error) { return json.Marshal(VersionJSON{ FormatVersion: formatVersion, - Time: v.Time, - Name: v.Name, - Email: v.Email, - Login: v.Login, - AvatarUrl: v.AvatarUrl, - Keys: v.Keys, - Nonce: v.Nonce, - Metadata: v.Metadata, + Time: v.time, + Name: v.name, + Email: v.email, + Login: v.login, + AvatarUrl: v.avatarURL, + Keys: v.keys, + Nonce: v.nonce, + Metadata: v.metadata, }) } @@ -84,56 +84,56 @@ func (v *Version) UnmarshalJSON(data []byte) error { return fmt.Errorf("unknown format version %v", aux.FormatVersion) } - v.Time = aux.Time - v.Name = aux.Name - v.Email = aux.Email - v.Login = aux.Login - v.AvatarUrl = aux.AvatarUrl - v.Keys = aux.Keys - v.Nonce = aux.Nonce - v.Metadata = aux.Metadata + v.time = aux.Time + v.name = aux.Name + v.email = aux.Email + v.login = aux.Login + v.avatarURL = aux.AvatarUrl + v.keys = aux.Keys + v.nonce = aux.Nonce + v.metadata = aux.Metadata return nil } func (v *Version) Validate() error { - if text.Empty(v.Name) && text.Empty(v.Login) { + if text.Empty(v.name) && text.Empty(v.login) { return fmt.Errorf("either name or login should be set") } - if strings.Contains(v.Name, "\n") { + if strings.Contains(v.name, "\n") { return fmt.Errorf("name should be a single line") } - if !text.Safe(v.Name) { + if !text.Safe(v.name) { return fmt.Errorf("name is not fully printable") } - if strings.Contains(v.Login, "\n") { + if strings.Contains(v.login, "\n") { return fmt.Errorf("login should be a single line") } - if !text.Safe(v.Login) { + if !text.Safe(v.login) { return fmt.Errorf("login is not fully printable") } - if strings.Contains(v.Email, "\n") { + if strings.Contains(v.email, "\n") { return fmt.Errorf("email should be a single line") } - if !text.Safe(v.Email) { + if !text.Safe(v.email) { return fmt.Errorf("email is not fully printable") } - if v.AvatarUrl != "" && !text.ValidUrl(v.AvatarUrl) { + if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) { return fmt.Errorf("avatarUrl is not a valid URL") } - if len(v.Nonce) > 64 { + if len(v.nonce) > 64 { return fmt.Errorf("nonce is too big") } - for _, k := range v.Keys { + for _, k := range v.keys { if err := k.Validate(); err != nil { return errors.Wrap(err, "invalid key") } @@ -178,20 +178,20 @@ func makeNonce(len int) []byte { // SetMetadata store arbitrary metadata about a version or an Identity in general // If the Version has been commit to git already, it won't be overwritten. func (v *Version) SetMetadata(key string, value string) { - if v.Metadata == nil { - v.Metadata = make(map[string]string) + if v.metadata == nil { + v.metadata = make(map[string]string) } - v.Metadata[key] = value + v.metadata[key] = value } // GetMetadata retrieve arbitrary metadata about the Version func (v *Version) GetMetadata(key string) (string, bool) { - val, ok := v.Metadata[key] + val, ok := v.metadata[key] return val, ok } // AllMetadata return all metadata for this Identity func (v *Version) AllMetadata() map[string]string { - return v.Metadata + return v.metadata } |