identity: wip

This commit is contained in:
Michael Muré 2019-02-06 22:06:42 +01:00 committed by Michael Muré
parent 328a4e5abf
commit 21048e785d
No known key found for this signature in database
GPG Key ID: A4457C029293126F
6 changed files with 242 additions and 200 deletions

View File

@ -459,6 +459,7 @@ func (bug *Bug) Commit(repo repository.ClockedRepo) error {
return err return err
} }
bug.staging.commitHash = hash
bug.packs = append(bug.packs, bug.staging) bug.packs = append(bug.packs, bug.staging)
bug.staging = OperationPack{} bug.staging = OperationPack{}
@ -513,9 +514,8 @@ func (bug *Bug) Merge(repo repository.Repo, other Interface) (bool, error) {
} }
ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit) ancestor, err := repo.FindCommonAncestor(bug.lastCommit, otherBug.lastCommit)
if err != nil { if err != nil {
return false, err return false, errors.Wrap(err, "can't find common ancestor")
} }
ancestorIndex := 0 ancestorIndex := 0

View File

@ -55,7 +55,7 @@ func TestBugValidity(t *testing.T) {
} }
} }
func TestBugSerialisation(t *testing.T) { func TestBugCommitLoad(t *testing.T) {
bug1 := NewBug() bug1 := NewBug()
bug1.Append(createOp) bug1.Append(createOp)
@ -69,22 +69,30 @@ func TestBugSerialisation(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
bug2, err := ReadLocalBug(repo, bug1.Id()) bug2, err := ReadLocalBug(repo, bug1.Id())
if err != nil { assert.NoError(t, err)
t.Error(err) equivalentBug(t, bug1, bug2)
}
// ignore some fields // add more op
bug2.packs[0].commitHash = bug1.packs[0].commitHash
for i := range bug1.packs[0].Operations {
bug2.packs[0].Operations[i].base().hash = bug1.packs[0].Operations[i].base().hash
}
// check hashes bug1.Append(setTitleOp)
for i := range bug1.packs[0].Operations { bug1.Append(addCommentOp)
if !bug2.packs[0].Operations[i].base().hash.IsValid() {
t.Fatal("invalid hash") err = bug1.Commit(repo)
assert.Nil(t, err)
bug3, err := ReadLocalBug(repo, bug1.Id())
assert.NoError(t, err)
equivalentBug(t, bug1, bug3)
}
func equivalentBug(t *testing.T, expected, actual *Bug) {
assert.Equal(t, len(expected.packs), len(actual.packs))
for i := range expected.packs {
for j := range expected.packs[i].Operations {
actual.packs[i].Operations[j].base().hash = expected.packs[i].Operations[j].base().hash
} }
} }
assert.Equal(t, bug1, bug2) assert.Equal(t, expected, actual)
} }

View File

@ -21,17 +21,22 @@ const identityConfigKey = "git-bug.identity"
var _ Interface = &Identity{} var _ Interface = &Identity{}
type Identity struct { type Identity struct {
id string // Id used as unique identifier
Versions []*Version id string
lastCommit git.Hash
// all the successive version of the identity
versions []*Version
} }
func NewIdentity(name string, email string) *Identity { func NewIdentity(name string, email string) *Identity {
return &Identity{ return &Identity{
Versions: []*Version{ versions: []*Version{
{ {
Name: name, name: name,
Email: email, email: email,
Nonce: makeNonce(20), 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 { func NewIdentityFull(name string, email string, login string, avatarUrl string) *Identity {
return &Identity{ return &Identity{
Versions: []*Version{ versions: []*Version{
{ {
Name: name, name: name,
Email: email, email: email,
Login: login, login: login,
AvatarUrl: avatarUrl, avatarURL: avatarUrl,
Nonce: makeNonce(20), nonce: makeNonce(20),
}, },
}, },
} }
@ -84,13 +89,15 @@ func read(repo repository.Repo, ref string) (*Identity, error) {
hashes, err := repo.ListCommits(ref) hashes, err := repo.ListCommits(ref)
var versions []*Version
// TODO: this is not perfect, it might be a command invoke error // TODO: this is not perfect, it might be a command invoke error
if err != nil { if err != nil {
return nil, ErrIdentityNotExist return nil, ErrIdentityNotExist
} }
i := &Identity{
id: id,
}
for _, hash := range hashes { for _, hash := range hashes {
entries, err := repo.ListEntries(hash) entries, err := repo.ListEntries(hash)
if err != nil { if err != nil {
@ -121,14 +128,12 @@ func read(repo repository.Repo, ref string) (*Identity, error) {
// tag the version with the commit hash // tag the version with the commit hash
version.commitHash = hash version.commitHash = hash
i.lastCommit = hash
versions = append(versions, &version) i.versions = append(i.versions, &version)
} }
return &Identity{ return i, nil
id: id,
Versions: versions,
}, nil
} }
// NewFromGitUser will query the repository for user detail and // 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) { 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 // 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 { func (i *Identity) Commit(repo repository.Repo) error {
// Todo: check for mismatch between memory and commited data // 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 != "" { if v.commitHash != "" {
lastCommit = v.commitHash i.lastCommit = v.commitHash
// ignore already commited versions // ignore already commited versions
continue continue
} }
@ -215,8 +218,8 @@ func (i *Identity) Commit(repo repository.Repo) error {
} }
var commitHash git.Hash var commitHash git.Hash
if lastCommit != "" { if i.lastCommit != "" {
commitHash, err = repo.StoreCommitWithParent(treeHash, lastCommit) commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit)
} else { } else {
commitHash, err = repo.StoreCommit(treeHash) commitHash, err = repo.StoreCommit(treeHash)
} }
@ -225,7 +228,8 @@ func (i *Identity) Commit(repo repository.Repo) error {
return err 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 it was the first commit, use the commit hash as the Identity id
if i.id == "" { if i.id == "" {
@ -238,7 +242,7 @@ func (i *Identity) Commit(repo repository.Repo) error {
} }
ref := fmt.Sprintf("%s%s", identityRefPattern, i.id) ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
err := repo.UpdateRef(ref, lastCommit) err := repo.UpdateRef(ref, i.lastCommit)
if err != nil { if err != nil {
return err return err
@ -247,31 +251,93 @@ func (i *Identity) Commit(repo repository.Repo) error {
return nil 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 // Validate check if the Identity data is valid
func (i *Identity) Validate() error { func (i *Identity) Validate() error {
lastTime := lamport.Time(0) lastTime := lamport.Time(0)
for _, v := range i.Versions { for _, v := range i.versions {
if err := v.Validate(); err != nil { if err := v.Validate(); err != nil {
return err return err
} }
if v.Time < lastTime { if v.time < lastTime {
return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.Time) return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time)
} }
lastTime = v.Time lastTime = v.time
} }
return nil return nil
} }
func (i *Identity) lastVersion() *Version { func (i *Identity) lastVersion() *Version {
if len(i.Versions) <= 0 { if len(i.versions) <= 0 {
panic("no version at all") panic("no version at all")
} }
return i.Versions[len(i.Versions)-1] return i.versions[len(i.versions)-1]
} }
// Id return the Identity identifier // Id return the Identity identifier
@ -286,27 +352,27 @@ func (i *Identity) Id() string {
// Name return the last version of the name // Name return the last version of the name
func (i *Identity) Name() string { func (i *Identity) Name() string {
return i.lastVersion().Name return i.lastVersion().name
} }
// Email return the last version of the email // Email return the last version of the email
func (i *Identity) Email() string { func (i *Identity) Email() string {
return i.lastVersion().Email return i.lastVersion().email
} }
// Login return the last version of the login // Login return the last version of the login
func (i *Identity) Login() string { 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 { 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 { 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. // 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 { func (i *Identity) ValidKeysAtTime(time lamport.Time) []Key {
var result []Key var result []Key
for _, v := range i.Versions { for _, v := range i.versions {
if v.Time > time { if v.time > time {
return result return result
} }
result = v.Keys result = v.keys
} }
return result return result
@ -357,8 +423,8 @@ func (i *Identity) SetMetadata(key string, value string) {
func (i *Identity) ImmutableMetadata() map[string]string { func (i *Identity) ImmutableMetadata() map[string]string {
metadata := make(map[string]string) metadata := make(map[string]string)
for _, version := range i.Versions { for _, version := range i.versions {
for key, value := range version.Metadata { for key, value := range version.metadata {
if _, has := metadata[key]; !has { if _, has := metadata[key]; !has {
metadata[key] = value metadata[key] = value
} }
@ -373,8 +439,8 @@ func (i *Identity) ImmutableMetadata() map[string]string {
func (i *Identity) MutableMetadata() map[string]string { func (i *Identity) MutableMetadata() map[string]string {
metadata := make(map[string]string) metadata := make(map[string]string)
for _, version := range i.Versions { for _, version := range i.versions {
for key, value := range version.Metadata { for key, value := range version.metadata {
metadata[key] = value metadata[key] = value
} }
} }

View File

@ -46,26 +46,6 @@ func Pull(repo repository.ClockedRepo, remote string) error {
} }
// MergeAll will merge all the available remote identity // 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 { func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
out := make(chan MergeResult) out := make(chan MergeResult)
@ -85,20 +65,19 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
id := refSplitted[len(refSplitted)-1] id := refSplitted[len(refSplitted)-1]
remoteIdentity, err := ReadLocal(repo, remoteRef) remoteIdentity, err := ReadLocal(repo, remoteRef)
remoteBug, err := readBug(repo, remoteRef)
if err != nil { 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 continue
} }
// Check for error in remote data // Check for error in remote data
if err := remoteBug.Validate(); err != nil { if err := remoteIdentity.Validate(); err != nil {
out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote bug is invalid").Error()) out <- newMergeInvalidStatus(id, errors.Wrap(err, "remote identity is invalid").Error())
continue continue
} }
localRef := bugsRefPattern + remoteBug.Id() localRef := identityRefPattern + remoteIdentity.Id()
localExist, err := repo.RefExist(localRef) localExist, err := repo.RefExist(localRef)
if err != nil { if err != nil {
@ -106,7 +85,7 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
continue continue
} }
// the bug is not local yet, simply create the reference // the identity is not local yet, simply create the reference
if !localExist { if !localExist {
err := repo.CopyRef(remoteRef, localRef) err := repo.CopyRef(remoteRef, localRef)
@ -115,18 +94,18 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
return return
} }
out <- newMergeStatus(MergeStatusNew, id, remoteBug) out <- newMergeStatus(MergeStatusNew, id, remoteIdentity)
continue continue
} }
localBug, err := readBug(repo, localRef) localIdentity, err := read(repo, localRef)
if err != nil { 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 return
} }
updated, err := localBug.Merge(repo, remoteBug) updated, err := localIdentity.Merge(repo, remoteIdentity)
if err != nil { if err != nil {
out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) out <- newMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error())
@ -134,9 +113,9 @@ func MergeAll(repo repository.ClockedRepo, remote string) <-chan MergeResult {
} }
if updated { if updated {
out <- newMergeStatus(MergeStatusUpdated, id, localBug) out <- newMergeStatus(MergeStatusUpdated, id, localIdentity)
} else { } else {
out <- newMergeStatus(MergeStatusNothing, id, localBug) out <- newMergeStatus(MergeStatusNothing, id, localIdentity)
} }
} }
}() }()

View File

@ -6,7 +6,6 @@ import (
"github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/repository"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
// Test the commit and load of an Identity with multiple versions // Test the commit and load of an Identity with multiple versions
@ -16,10 +15,10 @@ func TestIdentityCommitLoad(t *testing.T) {
// single version // single version
identity := &Identity{ identity := &Identity{
Versions: []*Version{ versions: []*Version{
{ {
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
}, },
}, },
} }
@ -32,33 +31,33 @@ func TestIdentityCommitLoad(t *testing.T) {
loaded, err := ReadLocal(mockRepo, identity.id) loaded, err := ReadLocal(mockRepo, identity.id)
assert.Nil(t, err) assert.Nil(t, err)
commitsAreSet(t, loaded) commitsAreSet(t, loaded)
equivalentIdentity(t, identity, loaded) assert.Equal(t, identity, loaded)
// multiple version // multiple version
identity = &Identity{ identity = &Identity{
Versions: []*Version{ versions: []*Version{
{ {
Time: 100, time: 100,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyA"}, {PubKey: "pubkeyA"},
}, },
}, },
{ {
Time: 200, time: 200,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyB"}, {PubKey: "pubkeyB"},
}, },
}, },
{ {
Time: 201, time: 201,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyC"}, {PubKey: "pubkeyC"},
}, },
}, },
@ -73,24 +72,24 @@ func TestIdentityCommitLoad(t *testing.T) {
loaded, err = ReadLocal(mockRepo, identity.id) loaded, err = ReadLocal(mockRepo, identity.id)
assert.Nil(t, err) assert.Nil(t, err)
commitsAreSet(t, loaded) commitsAreSet(t, loaded)
equivalentIdentity(t, identity, loaded) assert.Equal(t, identity, loaded)
// add more version // add more version
identity.AddVersion(&Version{ identity.AddVersion(&Version{
Time: 201, time: 201,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyD"}, {PubKey: "pubkeyD"},
}, },
}) })
identity.AddVersion(&Version{ identity.AddVersion(&Version{
Time: 300, time: 300,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyE"}, {PubKey: "pubkeyE"},
}, },
}) })
@ -103,66 +102,56 @@ func TestIdentityCommitLoad(t *testing.T) {
loaded, err = ReadLocal(mockRepo, identity.id) loaded, err = ReadLocal(mockRepo, identity.id)
assert.Nil(t, err) assert.Nil(t, err)
commitsAreSet(t, loaded) commitsAreSet(t, loaded)
equivalentIdentity(t, identity, loaded) assert.Equal(t, identity, loaded)
} }
func commitsAreSet(t *testing.T, identity *Identity) { func commitsAreSet(t *testing.T, identity *Identity) {
for _, version := range identity.Versions { for _, version := range identity.versions {
assert.NotEmpty(t, version.commitHash) 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 // Test that the correct crypto keys are returned for a given lamport time
func TestIdentity_ValidKeysAtTime(t *testing.T) { func TestIdentity_ValidKeysAtTime(t *testing.T) {
identity := Identity{ identity := Identity{
Versions: []*Version{ versions: []*Version{
{ {
Time: 100, time: 100,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyA"}, {PubKey: "pubkeyA"},
}, },
}, },
{ {
Time: 200, time: 200,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyB"}, {PubKey: "pubkeyB"},
}, },
}, },
{ {
Time: 201, time: 201,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyC"}, {PubKey: "pubkeyC"},
}, },
}, },
{ {
Time: 201, time: 201,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyD"}, {PubKey: "pubkeyD"},
}, },
}, },
{ {
Time: 300, time: 300,
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
Keys: []Key{ keys: []Key{
{PubKey: "pubkeyE"}, {PubKey: "pubkeyE"},
}, },
}, },
@ -197,8 +186,8 @@ func TestMetadata(t *testing.T) {
// try override // try override
identity.AddVersion(&Version{ identity.AddVersion(&Version{
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
}) })
identity.SetMetadata("key1", "value2") identity.SetMetadata("key1", "value2")
@ -226,10 +215,10 @@ func TestJSON(t *testing.T) {
mockRepo := repository.NewMockRepoForTest() mockRepo := repository.NewMockRepoForTest()
identity := &Identity{ identity := &Identity{
Versions: []*Version{ versions: []*Version{
{ {
Name: "René Descartes", name: "René Descartes",
Email: "rene.descartes@example.com", email: "rene.descartes@example.com",
}, },
}, },
} }

View File

@ -24,25 +24,25 @@ type Version struct {
// The lamport time at which this version become effective // The lamport time at which this version become effective
// The reference time is the bug edition lamport clock // The reference time is the bug edition lamport clock
Time lamport.Time time lamport.Time
Name string name string
Email string email string
Login string login string
AvatarUrl string avatarURL string
// The set of keys valid at that time, from this version onward, until they get removed // 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 // in a new version. This allow to have multiple key for the same identity (e.g. one per
// device) as well as revoke key. // 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. // 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 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. // 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. // 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 { type VersionJSON struct {
@ -62,14 +62,14 @@ type VersionJSON struct {
func (v *Version) MarshalJSON() ([]byte, error) { func (v *Version) MarshalJSON() ([]byte, error) {
return json.Marshal(VersionJSON{ return json.Marshal(VersionJSON{
FormatVersion: formatVersion, FormatVersion: formatVersion,
Time: v.Time, Time: v.time,
Name: v.Name, Name: v.name,
Email: v.Email, Email: v.email,
Login: v.Login, Login: v.login,
AvatarUrl: v.AvatarUrl, AvatarUrl: v.avatarURL,
Keys: v.Keys, Keys: v.keys,
Nonce: v.Nonce, Nonce: v.nonce,
Metadata: v.Metadata, Metadata: v.metadata,
}) })
} }
@ -84,56 +84,56 @@ func (v *Version) UnmarshalJSON(data []byte) error {
return fmt.Errorf("unknown format version %v", aux.FormatVersion) return fmt.Errorf("unknown format version %v", aux.FormatVersion)
} }
v.Time = aux.Time v.time = aux.Time
v.Name = aux.Name v.name = aux.Name
v.Email = aux.Email v.email = aux.Email
v.Login = aux.Login v.login = aux.Login
v.AvatarUrl = aux.AvatarUrl v.avatarURL = aux.AvatarUrl
v.Keys = aux.Keys v.keys = aux.Keys
v.Nonce = aux.Nonce v.nonce = aux.Nonce
v.Metadata = aux.Metadata v.metadata = aux.Metadata
return nil return nil
} }
func (v *Version) Validate() error { 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") 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") 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") 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") 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") 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") 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") 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") 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") return fmt.Errorf("nonce is too big")
} }
for _, k := range v.Keys { for _, k := range v.keys {
if err := k.Validate(); err != nil { if err := k.Validate(); err != nil {
return errors.Wrap(err, "invalid key") 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 // 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. // If the Version has been commit to git already, it won't be overwritten.
func (v *Version) SetMetadata(key string, value string) { func (v *Version) SetMetadata(key string, value string) {
if v.Metadata == nil { if v.metadata == nil {
v.Metadata = make(map[string]string) v.metadata = make(map[string]string)
} }
v.Metadata[key] = value v.metadata[key] = value
} }
// GetMetadata retrieve arbitrary metadata about the Version // GetMetadata retrieve arbitrary metadata about the Version
func (v *Version) GetMetadata(key string) (string, bool) { func (v *Version) GetMetadata(key string) (string, bool) {
val, ok := v.Metadata[key] val, ok := v.metadata[key]
return val, ok return val, ok
} }
// AllMetadata return all metadata for this Identity // AllMetadata return all metadata for this Identity
func (v *Version) AllMetadata() map[string]string { func (v *Version) AllMetadata() map[string]string {
return v.Metadata return v.metadata
} }