From 5511c230b678a181cc596238bf6669428d1b1902 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Thu, 18 Aug 2022 23:34:05 +0200 Subject: move {bug,identity} to /entities, move input to /commands --- identity/common.go | 37 --- identity/identity.go | 620 -------------------------------------- identity/identity_actions.go | 125 -------- identity/identity_actions_test.go | 157 ---------- identity/identity_stub.go | 101 ------- identity/identity_stub_test.go | 26 -- identity/identity_test.go | 292 ------------------ identity/identity_user.go | 68 ----- identity/interface.go | 62 ---- identity/key.go | 234 -------------- identity/key_test.go | 60 ---- identity/resolver.go | 34 --- identity/version.go | 273 ----------------- identity/version_test.go | 78 ----- 14 files changed, 2167 deletions(-) delete mode 100644 identity/common.go delete mode 100644 identity/identity.go delete mode 100644 identity/identity_actions.go delete mode 100644 identity/identity_actions_test.go delete mode 100644 identity/identity_stub.go delete mode 100644 identity/identity_stub_test.go delete mode 100644 identity/identity_test.go delete mode 100644 identity/identity_user.go delete mode 100644 identity/interface.go delete mode 100644 identity/key.go delete mode 100644 identity/key_test.go delete mode 100644 identity/resolver.go delete mode 100644 identity/version.go delete mode 100644 identity/version_test.go (limited to 'identity') diff --git a/identity/common.go b/identity/common.go deleted file mode 100644 index 5c6445e9..00000000 --- a/identity/common.go +++ /dev/null @@ -1,37 +0,0 @@ -package identity - -import ( - "encoding/json" - "errors" - "fmt" - - "github.com/MichaelMure/git-bug/entity" -) - -var ErrIdentityNotExist = errors.New("identity doesn't exist") - -func NewErrMultipleMatch(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("identity", matching) -} - -// Custom unmarshaling function to allow package user to delegate -// the decoding of an Identity and distinguish between an Identity -// and a Bare. -// -// If the given message has a "id" field, it's considered being a proper Identity. -func UnmarshalJSON(raw json.RawMessage) (Interface, error) { - aux := &IdentityStub{} - - // First try to decode and load as a normal Identity - err := json.Unmarshal(raw, &aux) - if err == nil && aux.Id() != "" { - return aux, nil - } - - // abort if we have an error other than the wrong type - if _, ok := err.(*json.UnmarshalTypeError); err != nil && !ok { - return nil, err - } - - return nil, fmt.Errorf("unknown identity type") -} diff --git a/identity/identity.go b/identity/identity.go deleted file mode 100644 index 0a7642af..00000000 --- a/identity/identity.go +++ /dev/null @@ -1,620 +0,0 @@ -// Package identity contains the identity data model and low-level related functions -package identity - -import ( - "encoding/json" - "fmt" - "reflect" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" - "github.com/MichaelMure/git-bug/util/timestamp" -) - -const identityRefPattern = "refs/identities/" -const identityRemoteRefPattern = "refs/remotes/%s/identities/" -const versionEntryName = "version" -const identityConfigKey = "git-bug.identity" - -var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge") -var ErrNoIdentitySet = errors.New("No identity is set.\n" + - "To interact with bugs, an identity first needs to be created using " + - "\"git bug user create\"") -var ErrMultipleIdentitiesSet = errors.New("multiple user identities set") - -func NewErrMultipleMatchIdentity(matching []entity.Id) *entity.ErrMultipleMatch { - return entity.NewErrMultipleMatch("identity", matching) -} - -var _ Interface = &Identity{} -var _ entity.Interface = &Identity{} - -type Identity struct { - // all the successive version of the identity - versions []*version -} - -func NewIdentity(repo repository.RepoClock, name string, email string) (*Identity, error) { - return NewIdentityFull(repo, name, email, "", "", nil) -} - -func NewIdentityFull(repo repository.RepoClock, name string, email string, login string, avatarUrl string, keys []*Key) (*Identity, error) { - v, err := newVersion(repo, name, email, login, avatarUrl, keys) - if err != nil { - return nil, err - } - return &Identity{ - versions: []*version{v}, - }, nil -} - -// NewFromGitUser will query the repository for user detail and -// build the corresponding Identity -func NewFromGitUser(repo repository.ClockedRepo) (*Identity, error) { - name, err := repo.GetUserName() - if err != nil { - return nil, err - } - if name == "" { - return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`") - } - - email, err := repo.GetUserEmail() - if err != nil { - return nil, err - } - if email == "" { - return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`") - } - - return NewIdentity(repo, name, email) -} - -// MarshalJSON will only serialize the id -func (i *Identity) MarshalJSON() ([]byte, error) { - return json.Marshal(&IdentityStub{ - id: i.Id(), - }) -} - -// UnmarshalJSON will only read the id -// Users of this package are expected to run Load() to load -// the remaining data from the identities data in git. -func (i *Identity) UnmarshalJSON(data []byte) error { - panic("identity should be loaded with identity.UnmarshalJSON") -} - -// ReadLocal load a local Identity from the identities data available in git -func ReadLocal(repo repository.Repo, id entity.Id) (*Identity, error) { - ref := fmt.Sprintf("%s%s", identityRefPattern, id) - return read(repo, ref) -} - -// ReadRemote load a remote Identity from the identities data available in git -func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, error) { - ref := fmt.Sprintf(identityRemoteRefPattern, remote) + id - return read(repo, ref) -} - -// read will load and parse an identity from git -func read(repo repository.Repo, ref string) (*Identity, error) { - id := entity.RefToId(ref) - - if err := id.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid ref") - } - - hashes, err := repo.ListCommits(ref) - if err != nil { - return nil, ErrIdentityNotExist - } - if len(hashes) == 0 { - return nil, fmt.Errorf("empty identity") - } - - i := &Identity{} - - for _, hash := range hashes { - entries, err := repo.ReadTree(hash) - if err != nil { - return nil, errors.Wrap(err, "can't list git tree entries") - } - if len(entries) != 1 { - return nil, fmt.Errorf("invalid identity data at hash %s", hash) - } - - entry := entries[0] - if entry.Name != versionEntryName { - return nil, fmt.Errorf("invalid identity data at hash %s", hash) - } - - data, err := repo.ReadData(entry.Hash) - if err != nil { - return nil, errors.Wrap(err, "failed to read git blob data") - } - - var version version - err = json.Unmarshal(data, &version) - if err != nil { - return nil, errors.Wrapf(err, "failed to decode Identity version json %s", hash) - } - - // tag the version with the commit hash - version.commitHash = hash - - i.versions = append(i.versions, &version) - } - - if id != i.versions[0].Id() { - return nil, fmt.Errorf("identity ID doesn't math the first version ID") - } - - return i, nil -} - -// ListLocalIds list all the available local identity ids -func ListLocalIds(repo repository.Repo) ([]entity.Id, error) { - refs, err := repo.ListRefs(identityRefPattern) - if err != nil { - return nil, err - } - - return entity.RefsToIds(refs), nil -} - -// RemoveIdentity will remove a local identity from its entity.Id -func RemoveIdentity(repo repository.ClockedRepo, id entity.Id) error { - var fullMatches []string - - refs, err := repo.ListRefs(identityRefPattern + id.String()) - if err != nil { - return err - } - if len(refs) > 1 { - return NewErrMultipleMatchIdentity(entity.RefsToIds(refs)) - } - if len(refs) == 1 { - // we have the identity locally - fullMatches = append(fullMatches, refs[0]) - } - - remotes, err := repo.GetRemotes() - if err != nil { - return err - } - - for remote := range remotes { - remotePrefix := fmt.Sprintf(identityRemoteRefPattern+id.String(), remote) - remoteRefs, err := repo.ListRefs(remotePrefix) - if err != nil { - return err - } - if len(remoteRefs) > 1 { - return NewErrMultipleMatchIdentity(entity.RefsToIds(refs)) - } - if len(remoteRefs) == 1 { - // found the identity in a remote - fullMatches = append(fullMatches, remoteRefs[0]) - } - } - - if len(fullMatches) == 0 { - return ErrIdentityNotExist - } - - for _, ref := range fullMatches { - err = repo.RemoveRef(ref) - if err != nil { - return err - } - } - - return nil -} - -type StreamedIdentity struct { - Identity *Identity - Err error -} - -// ReadAllLocal read and parse all local Identity -func ReadAllLocal(repo repository.ClockedRepo) <-chan StreamedIdentity { - return readAll(repo, identityRefPattern) -} - -// ReadAllRemote read and parse all remote Identity for a given remote -func ReadAllRemote(repo repository.ClockedRepo, remote string) <-chan StreamedIdentity { - refPrefix := fmt.Sprintf(identityRemoteRefPattern, remote) - return readAll(repo, refPrefix) -} - -// readAll read and parse all available bug with a given ref prefix -func readAll(repo repository.ClockedRepo, refPrefix string) <-chan StreamedIdentity { - out := make(chan StreamedIdentity) - - go func() { - defer close(out) - - refs, err := repo.ListRefs(refPrefix) - if err != nil { - out <- StreamedIdentity{Err: err} - return - } - - for _, ref := range refs { - b, err := read(repo, ref) - - if err != nil { - out <- StreamedIdentity{Err: err} - return - } - - out <- StreamedIdentity{Identity: b} - } - }() - - return out -} - -type Mutator struct { - Name string - Login string - Email string - AvatarUrl string - Keys []*Key -} - -// Mutate allow to create a new version of the Identity in one go -func (i *Identity) Mutate(repo repository.RepoClock, f func(orig *Mutator)) error { - copyKeys := func(keys []*Key) []*Key { - result := make([]*Key, len(keys)) - for i, key := range keys { - result[i] = key.Clone() - } - return result - } - - orig := Mutator{ - Name: i.Name(), - Email: i.Email(), - Login: i.Login(), - AvatarUrl: i.AvatarUrl(), - Keys: copyKeys(i.Keys()), - } - mutated := orig - mutated.Keys = copyKeys(orig.Keys) - - f(&mutated) - - if reflect.DeepEqual(orig, mutated) { - return nil - } - - v, err := newVersion(repo, - mutated.Name, - mutated.Email, - mutated.Login, - mutated.AvatarUrl, - mutated.Keys, - ) - if err != nil { - return err - } - - i.versions = append(i.versions, v) - return nil -} - -// Write the identity into the Repository. In particular, this ensure that -// the Id is properly set. -func (i *Identity) Commit(repo repository.ClockedRepo) error { - if !i.NeedCommit() { - return fmt.Errorf("can't commit an identity with no pending version") - } - - if err := i.Validate(); err != nil { - return errors.Wrap(err, "can't commit an identity with invalid data") - } - - var lastCommit repository.Hash - for _, v := range i.versions { - if v.commitHash != "" { - lastCommit = v.commitHash - // ignore already commit versions - continue - } - - blobHash, err := v.Write(repo) - if err != nil { - return err - } - - // Make a git tree referencing the blob - tree := []repository.TreeEntry{ - {ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName}, - } - - treeHash, err := repo.StoreTree(tree) - if err != nil { - return err - } - - var commitHash repository.Hash - if lastCommit != "" { - commitHash, err = repo.StoreCommit(treeHash, lastCommit) - } else { - commitHash, err = repo.StoreCommit(treeHash) - } - if err != nil { - return err - } - - lastCommit = commitHash - v.commitHash = commitHash - } - - ref := fmt.Sprintf("%s%s", identityRefPattern, i.Id().String()) - return repo.UpdateRef(ref, lastCommit) -} - -func (i *Identity) CommitAsNeeded(repo repository.ClockedRepo) error { - if !i.NeedCommit() { - return nil - } - return i.Commit(repo) -} - -func (i *Identity) NeedCommit() bool { - for _, v := range i.versions { - if v.commitHash == "" { - return true - } - } - - return false -} - -// 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 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 when 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 its 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") - } - - modified := false - var lastCommit repository.Hash - 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) - 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, ErrNonFastForwardMerge - } - } - - if modified { - err := repo.UpdateRef(identityRefPattern+i.Id().String(), lastCommit) - if err != nil { - return false, err - } - } - - return false, nil -} - -// Validate check if the Identity data is valid -func (i *Identity) Validate() error { - lastTimes := make(map[string]lamport.Time) - - if len(i.versions) == 0 { - return fmt.Errorf("no version") - } - - for _, v := range i.versions { - if err := v.Validate(); err != nil { - return err - } - - // check for always increasing lamport time - // check that a new version didn't drop a clock - for name, previous := range lastTimes { - if now, ok := v.times[name]; ok { - if now < previous { - return fmt.Errorf("non-chronological lamport clock %s (%d --> %d)", name, previous, now) - } - } else { - return fmt.Errorf("version has less lamport clocks than before (missing %s)", name) - } - } - - for name, now := range v.times { - lastTimes[name] = now - } - } - - return nil -} - -func (i *Identity) lastVersion() *version { - if len(i.versions) <= 0 { - panic("no version at all") - } - - return i.versions[len(i.versions)-1] -} - -// Id return the Identity identifier -func (i *Identity) Id() entity.Id { - // id is the id of the first version - return i.versions[0].Id() -} - -// Name return the last version of the name -func (i *Identity) Name() string { - return i.lastVersion().name -} - -// DisplayName return a non-empty string to display, representing the -// identity, based on the non-empty values. -func (i *Identity) DisplayName() string { - switch { - case i.Name() == "" && i.Login() != "": - return i.Login() - case i.Name() != "" && i.Login() == "": - return i.Name() - case i.Name() != "" && i.Login() != "": - return fmt.Sprintf("%s (%s)", i.Name(), i.Login()) - } - - panic("invalid person data") -} - -// Email return the last version of the email -func (i *Identity) Email() string { - return i.lastVersion().email -} - -// Login return the last version of the login -func (i *Identity) Login() string { - return i.lastVersion().login -} - -// AvatarUrl return the last version of the Avatar URL -func (i *Identity) AvatarUrl() string { - return i.lastVersion().avatarURL -} - -// Keys return the last version of the valid keys -func (i *Identity) Keys() []*Key { - return i.lastVersion().keys -} - -// SigningKey return the key that should be used to sign new messages. If no key is available, return nil. -func (i *Identity) SigningKey(repo repository.RepoKeyring) (*Key, error) { - keys := i.Keys() - for _, key := range keys { - err := key.ensurePrivateKey(repo) - if err == errNoPrivateKey { - continue - } - if err != nil { - return nil, err - } - return key, nil - } - return nil, nil -} - -// ValidKeysAtTime return the set of keys valid at a given lamport time -func (i *Identity) ValidKeysAtTime(clockName string, time lamport.Time) []*Key { - var result []*Key - - var lastTime lamport.Time - for _, v := range i.versions { - refTime, ok := v.times[clockName] - if !ok { - refTime = lastTime - } - lastTime = refTime - - if refTime > time { - return result - } - - result = v.keys - } - - return result -} - -// LastModification return the timestamp at which the last version of the identity became valid. -func (i *Identity) LastModification() timestamp.Timestamp { - return timestamp.Timestamp(i.lastVersion().unixTime) -} - -// LastModificationLamports return the lamport times at which the last version of the identity became valid. -func (i *Identity) LastModificationLamports() map[string]lamport.Time { - return i.lastVersion().times -} - -// IsProtected return true if the chain of git commits started to be signed. -// If that's the case, only signed commit with a valid key for this identity can be added. -func (i *Identity) IsProtected() bool { - // Todo - return false -} - -// SetMetadata store arbitrary metadata along the last not-commit version. -// If the version has been commit to git already, a new identical version is added and will need to be -// commit. -func (i *Identity) SetMetadata(key string, value string) { - // once commit, data is immutable so we create a new version - if i.lastVersion().commitHash != "" { - i.versions = append(i.versions, i.lastVersion().Clone()) - } - // if Id() has been called, we can't change the first version anymore, so we create a new version - if len(i.versions) == 1 && i.versions[0].id != entity.UnsetId && i.versions[0].id != "" { - i.versions = append(i.versions, i.lastVersion().Clone()) - } - - i.lastVersion().SetMetadata(key, value) -} - -// ImmutableMetadata return all metadata for this Identity, accumulated from each version. -// If multiple value are found, the first defined takes precedence. -func (i *Identity) ImmutableMetadata() map[string]string { - metadata := make(map[string]string) - - for _, version := range i.versions { - for key, value := range version.metadata { - if _, has := metadata[key]; !has { - metadata[key] = value - } - } - } - - return metadata -} - -// MutableMetadata return all metadata for this Identity, accumulated from each version. -// If multiple value are found, the last defined takes precedence. -func (i *Identity) MutableMetadata() map[string]string { - metadata := make(map[string]string) - - for _, version := range i.versions { - for key, value := range version.metadata { - metadata[key] = value - } - } - - return metadata -} diff --git a/identity/identity_actions.go b/identity/identity_actions.go deleted file mode 100644 index b58bb2d9..00000000 --- a/identity/identity_actions.go +++ /dev/null @@ -1,125 +0,0 @@ -package identity - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" -) - -// Fetch retrieve updates from a remote -// This does not change the local identities state -func Fetch(repo repository.Repo, remote string) (string, error) { - return repo.FetchRefs(remote, "identities") -} - -// Push update a remote with the local changes -func Push(repo repository.Repo, remote string) (string, error) { - return repo.PushRefs(remote, "identities") -} - -// Pull will do a Fetch + MergeAll -// This function will return an error if a merge fail -func Pull(repo repository.ClockedRepo, remote string) error { - _, err := Fetch(repo, remote) - if err != nil { - return err - } - - for merge := range MergeAll(repo, remote) { - if merge.Err != nil { - return merge.Err - } - if merge.Status == entity.MergeStatusInvalid { - return errors.Errorf("merge failure: %s", merge.Reason) - } - } - - return nil -} - -// MergeAll will merge all the available remote identity -func MergeAll(repo repository.ClockedRepo, remote string) <-chan entity.MergeResult { - out := make(chan entity.MergeResult) - - go func() { - defer close(out) - - remoteRefSpec := fmt.Sprintf(identityRemoteRefPattern, remote) - remoteRefs, err := repo.ListRefs(remoteRefSpec) - - if err != nil { - out <- entity.MergeResult{Err: err} - return - } - - for _, remoteRef := range remoteRefs { - refSplit := strings.Split(remoteRef, "/") - id := entity.Id(refSplit[len(refSplit)-1]) - - if err := id.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "invalid ref").Error()) - continue - } - - remoteIdentity, err := read(repo, remoteRef) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote identity is not readable").Error()) - continue - } - - // Check for error in remote data - if err := remoteIdentity.Validate(); err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "remote identity is invalid").Error()) - continue - } - - localRef := identityRefPattern + remoteIdentity.Id().String() - localExist, err := repo.RefExist(localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - continue - } - - // the identity is not local yet, simply create the reference - if !localExist { - err := repo.CopyRef(remoteRef, localRef) - - if err != nil { - out <- entity.NewMergeError(err, id) - return - } - - out <- entity.NewMergeNewStatus(id, remoteIdentity) - continue - } - - localIdentity, err := read(repo, localRef) - - if err != nil { - out <- entity.NewMergeError(errors.Wrap(err, "local identity is not readable"), id) - return - } - - updated, err := localIdentity.Merge(repo, remoteIdentity) - - if err != nil { - out <- entity.NewMergeInvalidStatus(id, errors.Wrap(err, "merge failed").Error()) - return - } - - if updated { - out <- entity.NewMergeUpdatedStatus(id, localIdentity) - } else { - out <- entity.NewMergeNothingStatus(id) - } - } - }() - - return out -} diff --git a/identity/identity_actions_test.go b/identity/identity_actions_test.go deleted file mode 100644 index 351fb7a4..00000000 --- a/identity/identity_actions_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package identity - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/repository" -) - -func TestIdentityPushPull(t *testing.T) { - repoA, repoB, _ := repository.SetupGoGitReposAndRemote(t) - - identity1, err := NewIdentity(repoA, "name1", "email1") - require.NoError(t, err) - err = identity1.Commit(repoA) - require.NoError(t, err) - - // A --> remote --> B - _, err = Push(repoA, "origin") - require.NoError(t, err) - - err = Pull(repoB, "origin") - require.NoError(t, err) - - identities := allIdentities(t, ReadAllLocal(repoB)) - - if len(identities) != 1 { - t.Fatal("Unexpected number of bugs") - } - - // B --> remote --> A - identity2, err := NewIdentity(repoB, "name2", "email2") - require.NoError(t, err) - err = identity2.Commit(repoB) - require.NoError(t, err) - - _, err = Push(repoB, "origin") - require.NoError(t, err) - - err = Pull(repoA, "origin") - require.NoError(t, err) - - identities = allIdentities(t, ReadAllLocal(repoA)) - - if len(identities) != 2 { - t.Fatal("Unexpected number of bugs") - } - - // Update both - - err = identity1.Mutate(repoA, func(orig *Mutator) { - orig.Name = "name1b" - orig.Email = "email1b" - }) - require.NoError(t, err) - err = identity1.Commit(repoA) - require.NoError(t, err) - - err = identity2.Mutate(repoB, func(orig *Mutator) { - orig.Name = "name2b" - orig.Email = "email2b" - }) - require.NoError(t, err) - err = identity2.Commit(repoB) - require.NoError(t, err) - - // A --> remote --> B - - _, err = Push(repoA, "origin") - require.NoError(t, err) - - err = Pull(repoB, "origin") - require.NoError(t, err) - - identities = allIdentities(t, ReadAllLocal(repoB)) - - if len(identities) != 2 { - t.Fatal("Unexpected number of bugs") - } - - // B --> remote --> A - - _, err = Push(repoB, "origin") - require.NoError(t, err) - - err = Pull(repoA, "origin") - require.NoError(t, err) - - identities = allIdentities(t, ReadAllLocal(repoA)) - - if len(identities) != 2 { - t.Fatal("Unexpected number of bugs") - } - - // Concurrent update - - err = identity1.Mutate(repoA, func(orig *Mutator) { - orig.Name = "name1c" - orig.Email = "email1c" - }) - require.NoError(t, err) - err = identity1.Commit(repoA) - require.NoError(t, err) - - identity1B, err := ReadLocal(repoB, identity1.Id()) - require.NoError(t, err) - - err = identity1B.Mutate(repoB, func(orig *Mutator) { - orig.Name = "name1concurrent" - orig.Email = "name1concurrent" - }) - require.NoError(t, err) - err = identity1B.Commit(repoB) - require.NoError(t, err) - - // A --> remote --> B - - _, err = Push(repoA, "origin") - require.NoError(t, err) - - // Pulling a non-fast-forward update should fail - err = Pull(repoB, "origin") - require.Error(t, err) - - identities = allIdentities(t, ReadAllLocal(repoB)) - - if len(identities) != 2 { - t.Fatal("Unexpected number of bugs") - } - - // B --> remote --> A - - // Pushing a non-fast-forward update should fail - _, err = Push(repoB, "origin") - require.Error(t, err) - - err = Pull(repoA, "origin") - require.NoError(t, err) - - identities = allIdentities(t, ReadAllLocal(repoA)) - - if len(identities) != 2 { - t.Fatal("Unexpected number of bugs") - } -} - -func allIdentities(t testing.TB, identities <-chan StreamedIdentity) []*Identity { - var result []*Identity - for streamed := range identities { - if streamed.Err != nil { - t.Fatal(streamed.Err) - } - result = append(result, streamed.Identity) - } - return result -} diff --git a/identity/identity_stub.go b/identity/identity_stub.go deleted file mode 100644 index fb5c90a5..00000000 --- a/identity/identity_stub.go +++ /dev/null @@ -1,101 +0,0 @@ -package identity - -import ( - "encoding/json" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" - "github.com/MichaelMure/git-bug/util/timestamp" -) - -var _ Interface = &IdentityStub{} - -// IdentityStub is an almost empty Identity, holding only the id. -// When a normal Identity is serialized into JSON, only the id is serialized. -// All the other data are stored in git in a chain of commit + a ref. -// When this JSON is deserialized, an IdentityStub is returned instead, to be replaced -// later by the proper Identity, loaded from the Repo. -type IdentityStub struct { - id entity.Id -} - -func (i *IdentityStub) MarshalJSON() ([]byte, error) { - // TODO: add a type marker - return json.Marshal(struct { - Id entity.Id `json:"id"` - }{ - Id: i.id, - }) -} - -func (i *IdentityStub) UnmarshalJSON(data []byte) error { - aux := struct { - Id entity.Id `json:"id"` - }{} - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - i.id = aux.Id - - return nil -} - -// Id return the Identity identifier -func (i *IdentityStub) Id() entity.Id { - return i.id -} - -func (IdentityStub) Name() string { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) DisplayName() string { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) Email() string { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) Login() string { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) AvatarUrl() string { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) Keys() []*Key { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (i *IdentityStub) SigningKey(repo repository.RepoKeyring) (*Key, error) { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) ValidKeysAtTime(_ string, _ lamport.Time) []*Key { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (i *IdentityStub) LastModification() timestamp.Timestamp { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (i *IdentityStub) LastModificationLamports() map[string]lamport.Time { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) IsProtected() bool { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (IdentityStub) Validate() error { - panic("identities needs to be properly loaded with identity.ReadLocal()") -} - -func (i *IdentityStub) NeedCommit() bool { - return false -} diff --git a/identity/identity_stub_test.go b/identity/identity_stub_test.go deleted file mode 100644 index b01a718c..00000000 --- a/identity/identity_stub_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package identity - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestIdentityStubSerialize(t *testing.T) { - before := &IdentityStub{ - id: "id1234", - } - - data, err := json.Marshal(before) - assert.NoError(t, err) - - var after IdentityStub - err = json.Unmarshal(data, &after) - assert.NoError(t, err) - - // enforce creating the Id - before.Id() - - assert.Equal(t, before, &after) -} diff --git a/identity/identity_test.go b/identity/identity_test.go deleted file mode 100644 index f0c3bbe9..00000000 --- a/identity/identity_test.go +++ /dev/null @@ -1,292 +0,0 @@ -package identity - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -// Test the commit and load of an Identity with multiple versions -func TestIdentityCommitLoad(t *testing.T) { - repo := makeIdentityTestRepo(t) - - // single version - - identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") - require.NoError(t, err) - - idBeforeCommit := identity.Id() - - err = identity.Commit(repo) - require.NoError(t, err) - - commitsAreSet(t, identity) - require.NotEmpty(t, identity.Id()) - require.Equal(t, idBeforeCommit, identity.Id()) - require.Equal(t, idBeforeCommit, identity.versions[0].Id()) - - loaded, err := ReadLocal(repo, identity.Id()) - require.NoError(t, err) - commitsAreSet(t, loaded) - require.Equal(t, identity, loaded) - - // multiple versions - - identity, err = NewIdentityFull(repo, "René Descartes", "rene.descartes@example.com", "", "", []*Key{generatePublicKey()}) - require.NoError(t, err) - - idBeforeCommit = identity.Id() - - err = identity.Mutate(repo, func(orig *Mutator) { - orig.Keys = []*Key{generatePublicKey()} - }) - require.NoError(t, err) - - err = identity.Mutate(repo, func(orig *Mutator) { - orig.Keys = []*Key{generatePublicKey()} - }) - require.NoError(t, err) - - require.Equal(t, idBeforeCommit, identity.Id()) - - err = identity.Commit(repo) - require.NoError(t, err) - - commitsAreSet(t, identity) - require.NotEmpty(t, identity.Id()) - require.Equal(t, idBeforeCommit, identity.Id()) - require.Equal(t, idBeforeCommit, identity.versions[0].Id()) - - loaded, err = ReadLocal(repo, identity.Id()) - require.NoError(t, err) - commitsAreSet(t, loaded) - require.Equal(t, identity, loaded) - - // add more version - - err = identity.Mutate(repo, func(orig *Mutator) { - orig.Email = "rene@descartes.com" - orig.Keys = []*Key{generatePublicKey()} - }) - require.NoError(t, err) - - err = identity.Mutate(repo, func(orig *Mutator) { - orig.Email = "rene@descartes.com" - orig.Keys = []*Key{generatePublicKey(), generatePublicKey()} - }) - require.NoError(t, err) - - err = identity.Commit(repo) - require.NoError(t, err) - - commitsAreSet(t, identity) - require.NotEmpty(t, identity.Id()) - require.Equal(t, idBeforeCommit, identity.Id()) - require.Equal(t, idBeforeCommit, identity.versions[0].Id()) - - loaded, err = ReadLocal(repo, identity.Id()) - require.NoError(t, err) - commitsAreSet(t, loaded) - require.Equal(t, identity, loaded) -} - -func TestIdentityMutate(t *testing.T) { - repo := makeIdentityTestRepo(t) - - identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") - require.NoError(t, err) - - require.Len(t, identity.versions, 1) - - err = identity.Mutate(repo, func(orig *Mutator) { - orig.Email = "rene@descartes.fr" - orig.Name = "René" - orig.Login = "rene" - }) - require.NoError(t, err) - - require.Len(t, identity.versions, 2) - require.Equal(t, identity.Email(), "rene@descartes.fr") - require.Equal(t, identity.Name(), "René") - require.Equal(t, identity.Login(), "rene") -} - -func commitsAreSet(t *testing.T, identity *Identity) { - for _, version := range identity.versions { - require.NotEmpty(t, version.commitHash) - } -} - -// Test that the correct crypto keys are returned for a given lamport time -func TestIdentity_ValidKeysAtTime(t *testing.T) { - pubKeyA := generatePublicKey() - pubKeyB := generatePublicKey() - pubKeyC := generatePublicKey() - pubKeyD := generatePublicKey() - pubKeyE := generatePublicKey() - - identity := Identity{ - versions: []*version{ - { - times: map[string]lamport.Time{"foo": 100}, - keys: []*Key{pubKeyA}, - }, - { - times: map[string]lamport.Time{"foo": 200}, - keys: []*Key{pubKeyB}, - }, - { - times: map[string]lamport.Time{"foo": 201}, - keys: []*Key{pubKeyC}, - }, - { - times: map[string]lamport.Time{"foo": 201}, - keys: []*Key{pubKeyD}, - }, - { - times: map[string]lamport.Time{"foo": 300}, - keys: []*Key{pubKeyE}, - }, - }, - } - - require.Nil(t, identity.ValidKeysAtTime("foo", 10)) - require.Equal(t, identity.ValidKeysAtTime("foo", 100), []*Key{pubKeyA}) - require.Equal(t, identity.ValidKeysAtTime("foo", 140), []*Key{pubKeyA}) - require.Equal(t, identity.ValidKeysAtTime("foo", 200), []*Key{pubKeyB}) - require.Equal(t, identity.ValidKeysAtTime("foo", 201), []*Key{pubKeyD}) - require.Equal(t, identity.ValidKeysAtTime("foo", 202), []*Key{pubKeyD}) - require.Equal(t, identity.ValidKeysAtTime("foo", 300), []*Key{pubKeyE}) - require.Equal(t, identity.ValidKeysAtTime("foo", 3000), []*Key{pubKeyE}) -} - -// Test the immutable or mutable metadata search -func TestMetadata(t *testing.T) { - repo := makeIdentityTestRepo(t) - - identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") - require.NoError(t, err) - - identity.SetMetadata("key1", "value1") - assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") - assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1") - - err = identity.Commit(repo) - require.NoError(t, err) - - assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") - assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value1") - - // try override - err = identity.Mutate(repo, func(orig *Mutator) { - orig.Email = "rene@descartes.fr" - }) - require.NoError(t, err) - - identity.SetMetadata("key1", "value2") - assertHasKeyValue(t, identity.ImmutableMetadata(), "key1", "value1") - assertHasKeyValue(t, identity.MutableMetadata(), "key1", "value2") - - err = identity.Commit(repo) - require.NoError(t, err) - - // reload - loaded, err := ReadLocal(repo, identity.Id()) - require.NoError(t, err) - - assertHasKeyValue(t, loaded.ImmutableMetadata(), "key1", "value1") - assertHasKeyValue(t, loaded.MutableMetadata(), "key1", "value2") - - // set metadata after commit - versionCount := len(identity.versions) - identity.SetMetadata("foo", "bar") - require.True(t, identity.NeedCommit()) - require.Len(t, identity.versions, versionCount+1) - - err = identity.Commit(repo) - require.NoError(t, err) - require.Len(t, identity.versions, versionCount+1) -} - -func assertHasKeyValue(t *testing.T, metadata map[string]string, key, value string) { - val, ok := metadata[key] - require.True(t, ok) - require.Equal(t, val, value) -} - -func TestJSON(t *testing.T) { - repo := makeIdentityTestRepo(t) - - identity, err := NewIdentity(repo, "René Descartes", "rene.descartes@example.com") - require.NoError(t, err) - - // commit to make sure we have an Id - err = identity.Commit(repo) - require.NoError(t, err) - require.NotEmpty(t, identity.Id()) - - // serialize - data, err := json.Marshal(identity) - require.NoError(t, err) - - // deserialize, got a IdentityStub with the same id - var i Interface - i, err = UnmarshalJSON(data) - require.NoError(t, err) - require.Equal(t, identity.Id(), i.Id()) - - // make sure we can load the identity properly - i, err = ReadLocal(repo, i.Id()) - require.NoError(t, err) -} - -func TestIdentityRemove(t *testing.T) { - repo := repository.CreateGoGitTestRepo(t, false) - remoteA := repository.CreateGoGitTestRepo(t, true) - remoteB := repository.CreateGoGitTestRepo(t, true) - - err := repo.AddRemote("remoteA", remoteA.GetLocalRemote()) - require.NoError(t, err) - - err = repo.AddRemote("remoteB", remoteB.GetLocalRemote()) - require.NoError(t, err) - - // generate an identity for testing - rene, err := NewIdentity(repo, "René Descartes", "rene@descartes.fr") - require.NoError(t, err) - - err = rene.Commit(repo) - require.NoError(t, err) - - _, err = Push(repo, "remoteA") - require.NoError(t, err) - - _, err = Push(repo, "remoteB") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteA") - require.NoError(t, err) - - _, err = Fetch(repo, "remoteB") - require.NoError(t, err) - - err = RemoveIdentity(repo, rene.Id()) - require.NoError(t, err) - - _, err = ReadLocal(repo, rene.Id()) - require.Error(t, ErrIdentityNotExist, err) - - _, err = ReadRemote(repo, "remoteA", string(rene.Id())) - require.Error(t, ErrIdentityNotExist, err) - - _, err = ReadRemote(repo, "remoteB", string(rene.Id())) - require.Error(t, ErrIdentityNotExist, err) - - ids, err := ListLocalIds(repo) - require.NoError(t, err) - require.Len(t, ids, 0) -} diff --git a/identity/identity_user.go b/identity/identity_user.go deleted file mode 100644 index cd67459e..00000000 --- a/identity/identity_user.go +++ /dev/null @@ -1,68 +0,0 @@ -package identity - -import ( - "fmt" - "os" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" -) - -// SetUserIdentity store the user identity's id in the git config -func SetUserIdentity(repo repository.RepoConfig, identity *Identity) error { - return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String()) -} - -// GetUserIdentity read the current user identity, set with a git config entry -func GetUserIdentity(repo repository.Repo) (*Identity, error) { - id, err := GetUserIdentityId(repo) - if err != nil { - return nil, err - } - - i, err := ReadLocal(repo, id) - if err == ErrIdentityNotExist { - innerErr := repo.LocalConfig().RemoveAll(identityConfigKey) - if innerErr != nil { - _, _ = fmt.Fprintln(os.Stderr, errors.Wrap(innerErr, "can't clear user identity").Error()) - } - return nil, err - } - - return i, nil -} - -func GetUserIdentityId(repo repository.Repo) (entity.Id, error) { - val, err := repo.LocalConfig().ReadString(identityConfigKey) - if err == repository.ErrNoConfigEntry { - return entity.UnsetId, ErrNoIdentitySet - } - if err == repository.ErrMultipleConfigEntry { - return entity.UnsetId, ErrMultipleIdentitiesSet - } - if err != nil { - return entity.UnsetId, err - } - - var id = entity.Id(val) - - if err := id.Validate(); err != nil { - return entity.UnsetId, err - } - - return id, nil -} - -// IsUserIdentitySet say if the user has set his identity -func IsUserIdentitySet(repo repository.Repo) (bool, error) { - _, err := repo.LocalConfig().ReadString(identityConfigKey) - if err == repository.ErrNoConfigEntry { - return false, nil - } - if err != nil { - return false, err - } - return true, nil -} diff --git a/identity/interface.go b/identity/interface.go deleted file mode 100644 index c6e22e00..00000000 --- a/identity/interface.go +++ /dev/null @@ -1,62 +0,0 @@ -package identity - -import ( - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" - "github.com/MichaelMure/git-bug/util/timestamp" -) - -type Interface interface { - entity.Interface - - // Name return the last version of the name - // Can be empty. - Name() string - - // DisplayName return a non-empty string to display, representing the - // identity, based on the non-empty values. - DisplayName() string - - // Email return the last version of the email - // Can be empty. - Email() string - - // Login return the last version of the login - // Can be empty. - // Warning: this login can be defined when importing from a bridge but should *not* be - // used to identify an identity as multiple bridge with different login can map to the same - // identity. Use the metadata system for that usage instead. - Login() string - - // AvatarUrl return the last version of the Avatar URL - // Can be empty. - AvatarUrl() string - - // Keys return the last version of the valid keys - // Can be empty. - Keys() []*Key - - // SigningKey return the key that should be used to sign new messages. If no key is available, return nil. - SigningKey(repo repository.RepoKeyring) (*Key, error) - - // ValidKeysAtTime return the set of keys valid at a given lamport time for a given clock of another entity - // Can be empty. - ValidKeysAtTime(clockName string, time lamport.Time) []*Key - - // LastModification return the timestamp at which the last version of the identity became valid. - LastModification() timestamp.Timestamp - - // LastModificationLamports return the lamport times at which the last version of the identity became valid. - LastModificationLamports() map[string]lamport.Time - - // IsProtected return true if the chain of git commits started to be signed. - // If that's the case, only signed commit with a valid key for this identity can be added. - IsProtected() bool - - // Validate check if the Identity data is valid - Validate() error - - // NeedCommit indicate that the in-memory state changed and need to be committed in the repository - NeedCommit() bool -} diff --git a/identity/key.go b/identity/key.go deleted file mode 100644 index 82b9b95c..00000000 --- a/identity/key.go +++ /dev/null @@ -1,234 +0,0 @@ -package identity - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "strings" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/repository" -) - -var errNoPrivateKey = fmt.Errorf("no private key") - -type Key struct { - public *packet.PublicKey - private *packet.PrivateKey -} - -// GenerateKey generate a keypair (public+private) -// The type and configuration of the key is determined by the default value in go's OpenPGP. -func GenerateKey() *Key { - entity, err := openpgp.NewEntity("", "", "", &packet.Config{ - // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. - // We don't care about the creation time so we can set it to the zero value. - Time: func() time.Time { - return time.Time{} - }, - }) - if err != nil { - panic(err) - } - return &Key{ - public: entity.PrimaryKey, - private: entity.PrivateKey, - } -} - -// generatePublicKey generate only a public key (only useful for testing) -// See GenerateKey for the details. -func generatePublicKey() *Key { - k := GenerateKey() - k.private = nil - return k -} - -func (k *Key) Public() *packet.PublicKey { - return k.public -} - -func (k *Key) Private() *packet.PrivateKey { - return k.private -} - -func (k *Key) Validate() error { - if k.public == nil { - return fmt.Errorf("nil public key") - } - if !k.public.CanSign() { - return fmt.Errorf("public key can't sign") - } - - if k.private != nil { - if !k.private.CanSign() { - return fmt.Errorf("private key can't sign") - } - } - - return nil -} - -func (k *Key) Clone() *Key { - clone := &Key{} - - pub := *k.public - clone.public = &pub - - if k.private != nil { - priv := *k.private - clone.private = &priv - } - - return clone -} - -func (k *Key) MarshalJSON() ([]byte, error) { - // Serialize only the public key, in the armored format. - var buf bytes.Buffer - w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) - if err != nil { - return nil, err - } - - err = k.public.Serialize(w) - if err != nil { - return nil, err - } - err = w.Close() - if err != nil { - return nil, err - } - return json.Marshal(buf.String()) -} - -func (k *Key) UnmarshalJSON(data []byte) error { - // De-serialize only the public key, in the armored format. - var armored string - err := json.Unmarshal(data, &armored) - if err != nil { - return err - } - - block, err := armor.Decode(strings.NewReader(armored)) - if err == io.EOF { - return fmt.Errorf("no armored data found") - } - if err != nil { - return err - } - - if block.Type != openpgp.PublicKeyType { - return fmt.Errorf("invalid key type") - } - - p, err := packet.Read(block.Body) - if err != nil { - return errors.Wrap(err, "failed to read public key packet") - } - - public, ok := p.(*packet.PublicKey) - if !ok { - return errors.New("got no packet.publicKey") - } - - // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. - // We don't care about the creation time so we can set it to the zero value. - public.CreationTime = time.Time{} - - k.public = public - return nil -} - -func (k *Key) loadPrivate(repo repository.RepoKeyring) error { - item, err := repo.Keyring().Get(k.public.KeyIdString()) - if err == repository.ErrKeyringKeyNotFound { - return errNoPrivateKey - } - if err != nil { - return err - } - - block, err := armor.Decode(bytes.NewReader(item.Data)) - if err == io.EOF { - return fmt.Errorf("no armored data found") - } - if err != nil { - return err - } - - if block.Type != openpgp.PrivateKeyType { - return fmt.Errorf("invalid key type") - } - - p, err := packet.Read(block.Body) - if err != nil { - return errors.Wrap(err, "failed to read private key packet") - } - - private, ok := p.(*packet.PrivateKey) - if !ok { - return errors.New("got no packet.privateKey") - } - - // The armored format doesn't include the creation time, which makes the round-trip data not being fully equal. - // We don't care about the creation time so we can set it to the zero value. - private.CreationTime = time.Time{} - - k.private = private - return nil -} - -// ensurePrivateKey attempt to load the corresponding private key if it is not loaded already. -// If no private key is found, returns errNoPrivateKey -func (k *Key) ensurePrivateKey(repo repository.RepoKeyring) error { - if k.private != nil { - return nil - } - - return k.loadPrivate(repo) -} - -func (k *Key) storePrivate(repo repository.RepoKeyring) error { - var buf bytes.Buffer - w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil) - if err != nil { - return err - } - err = k.private.Serialize(w) - if err != nil { - return err - } - err = w.Close() - if err != nil { - return err - } - - return repo.Keyring().Set(repository.Item{ - Key: k.public.KeyIdString(), - Data: buf.Bytes(), - }) -} - -func (k *Key) PGPEntity() *openpgp.Entity { - uid := packet.NewUserId("", "", "") - return &openpgp.Entity{ - PrimaryKey: k.public, - PrivateKey: k.private, - Identities: map[string]*openpgp.Identity{ - uid.Id: { - Name: uid.Id, - UserId: uid, - SelfSignature: &packet.Signature{ - IsPrimaryId: func() *bool { b := true; return &b }(), - }, - }, - }, - } -} diff --git a/identity/key_test.go b/identity/key_test.go deleted file mode 100644 index 6e320dc2..00000000 --- a/identity/key_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package identity - -import ( - "crypto/rsa" - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/repository" -) - -func TestPublicKeyJSON(t *testing.T) { - k := generatePublicKey() - - dataJSON, err := json.Marshal(k) - require.NoError(t, err) - - var read Key - err = json.Unmarshal(dataJSON, &read) - require.NoError(t, err) - - require.Equal(t, k, &read) -} - -func TestStoreLoad(t *testing.T) { - repo := repository.NewMockRepoKeyring() - - // public + private - k := GenerateKey() - - // Store - - dataJSON, err := json.Marshal(k) - require.NoError(t, err) - - err = k.storePrivate(repo) - require.NoError(t, err) - - // Load - - var read Key - err = json.Unmarshal(dataJSON, &read) - require.NoError(t, err) - - err = read.ensurePrivateKey(repo) - require.NoError(t, err) - - require.Equal(t, k.public, read.public) - - require.IsType(t, (*rsa.PrivateKey)(nil), k.private.PrivateKey) - - // See https://github.com/golang/crypto/pull/175 - rsaPriv := read.private.PrivateKey.(*rsa.PrivateKey) - back := rsaPriv.Primes[0] - rsaPriv.Primes[0] = rsaPriv.Primes[1] - rsaPriv.Primes[1] = back - - require.True(t, k.private.PrivateKey.(*rsa.PrivateKey).Equal(read.private.PrivateKey)) -} diff --git a/identity/resolver.go b/identity/resolver.go deleted file mode 100644 index 5468a8f8..00000000 --- a/identity/resolver.go +++ /dev/null @@ -1,34 +0,0 @@ -package identity - -import ( - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" -) - -var _ entity.Resolver = &SimpleResolver{} - -// SimpleResolver is a Resolver loading Identities directly from a Repo -type SimpleResolver struct { - repo repository.Repo -} - -func NewSimpleResolver(repo repository.Repo) *SimpleResolver { - return &SimpleResolver{repo: repo} -} - -func (r *SimpleResolver) Resolve(id entity.Id) (entity.Interface, error) { - return ReadLocal(r.repo, id) -} - -var _ entity.Resolver = &StubResolver{} - -// StubResolver is a Resolver that doesn't load anything, only returning IdentityStub instances -type StubResolver struct{} - -func NewStubResolver() *StubResolver { - return &StubResolver{} -} - -func (s *StubResolver) Resolve(id entity.Id) (entity.Interface, error) { - return &IdentityStub{id: id}, nil -} diff --git a/identity/version.go b/identity/version.go deleted file mode 100644 index 9a52d089..00000000 --- a/identity/version.go +++ /dev/null @@ -1,273 +0,0 @@ -package identity - -import ( - "crypto/rand" - "encoding/json" - "fmt" - "time" - - "github.com/pkg/errors" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" - "github.com/MichaelMure/git-bug/util/text" -) - -// 1: original format -// 2: Identity Ids are generated from the first version serialized data instead of from the first git commit -// + Identity hold multiple lamport clocks from other entities, instead of just bug edit -const formatVersion = 2 - -// version is a complete set of information about an Identity at a point in time. -type version struct { - name string - email string // as defined in git or from a bridge when importing the identity - login string // from a bridge when importing the identity - avatarURL string - - // The lamport times of the other entities at which this version become effective - times map[string]lamport.Time - unixTime int64 - - // 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 - - // mandatory random bytes to ensure a better randomness of the data of the first - // version of an identity, used to later generate the ID - // len(Nonce) should be > 20 and < 64 bytes - // It has no functional purpose and should be ignored. - // TODO: optional after first version? - nonce []byte - - // A set of arbitrary key/value to store metadata about a version or about an Identity in general. - metadata map[string]string - - // Not serialized. Store the version's id in memory. - id entity.Id - // Not serialized - commitHash repository.Hash -} - -func newVersion(repo repository.RepoClock, name string, email string, login string, avatarURL string, keys []*Key) (*version, error) { - clocks, err := repo.AllClocks() - if err != nil { - return nil, err - } - - times := make(map[string]lamport.Time) - for name, clock := range clocks { - times[name] = clock.Time() - } - - return &version{ - id: entity.UnsetId, - name: name, - email: email, - login: login, - avatarURL: avatarURL, - times: times, - unixTime: time.Now().Unix(), - keys: keys, - nonce: makeNonce(20), - }, nil -} - -type versionJSON struct { - // Additional field to version the data - FormatVersion uint `json:"version"` - - Times map[string]lamport.Time `json:"times"` - UnixTime int64 `json:"unix_time"` - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - Login string `json:"login,omitempty"` - AvatarUrl string `json:"avatar_url,omitempty"` - Keys []*Key `json:"pub_keys,omitempty"` - Nonce []byte `json:"nonce"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// Id return the identifier of the version -func (v *version) Id() entity.Id { - if v.id == "" { - // something went really wrong - panic("version's id not set") - } - if v.id == entity.UnsetId { - // This means we are trying to get the version's Id *before* it has been stored. - // As the Id is computed based on the actual bytes written on the disk, we are going to predict - // those and then get the Id. This is safe as it will be the exact same code writing on disk later. - data, err := json.Marshal(v) - if err != nil { - panic(err) - } - v.id = entity.DeriveId(data) - } - return v.id -} - -// Make a deep copy -func (v *version) Clone() *version { - // copy direct fields - clone := *v - - // reset some fields - clone.commitHash = "" - clone.id = entity.UnsetId - - clone.times = make(map[string]lamport.Time) - for name, t := range v.times { - clone.times[name] = t - } - - clone.keys = make([]*Key, len(v.keys)) - for i, key := range v.keys { - clone.keys[i] = key.Clone() - } - - clone.nonce = make([]byte, len(v.nonce)) - copy(clone.nonce, v.nonce) - - // not copying metadata - - return &clone -} - -func (v *version) MarshalJSON() ([]byte, error) { - return json.Marshal(versionJSON{ - FormatVersion: formatVersion, - Times: v.times, - UnixTime: v.unixTime, - Name: v.name, - Email: v.email, - Login: v.login, - AvatarUrl: v.avatarURL, - Keys: v.keys, - Nonce: v.nonce, - Metadata: v.metadata, - }) -} - -func (v *version) UnmarshalJSON(data []byte) error { - var aux versionJSON - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - if aux.FormatVersion != formatVersion { - return entity.NewErrInvalidFormat(aux.FormatVersion, formatVersion) - } - - v.id = entity.DeriveId(data) - v.times = aux.Times - v.unixTime = aux.UnixTime - 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 { - // time must be set after a commit - if v.commitHash != "" && v.unixTime == 0 { - return fmt.Errorf("unix time not set") - } - - if text.Empty(v.name) && text.Empty(v.login) { - return fmt.Errorf("either name or login should be set") - } - if !text.SafeOneLine(v.name) { - return fmt.Errorf("name has unsafe characters") - } - - if !text.SafeOneLine(v.login) { - return fmt.Errorf("login has unsafe characters") - } - - if !text.SafeOneLine(v.email) { - return fmt.Errorf("email has unsafe characters") - } - - if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) { - return fmt.Errorf("avatarUrl is not a valid URL") - } - - if len(v.nonce) > 64 { - return fmt.Errorf("nonce is too big") - } - if len(v.nonce) < 20 { - return fmt.Errorf("nonce is too small") - } - - for _, k := range v.keys { - if err := k.Validate(); err != nil { - return errors.Wrap(err, "invalid key") - } - } - - return nil -} - -// Write will serialize and store the version as a git blob and return -// its hash -func (v *version) Write(repo repository.Repo) (repository.Hash, error) { - // make sure we don't write invalid data - err := v.Validate() - if err != nil { - return "", errors.Wrap(err, "validation error") - } - - data, err := json.Marshal(v) - if err != nil { - return "", err - } - - hash, err := repo.StoreData(data) - if err != nil { - return "", err - } - - // make sure we set the Id when writing in the repo - v.id = entity.DeriveId(data) - - return hash, nil -} - -func makeNonce(len int) []byte { - result := make([]byte, len) - _, err := rand.Read(result) - if err != nil { - panic(err) - } - return result -} - -// 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. -// Beware: changing the metadata on a version will change it's ID -func (v *version) SetMetadata(key string, value string) { - if v.metadata == nil { - v.metadata = make(map[string]string) - } - v.metadata[key] = value -} - -// GetMetadata retrieve arbitrary metadata about the version -func (v *version) GetMetadata(key string) (string, bool) { - val, ok := v.metadata[key] - return val, ok -} - -// AllMetadata return all metadata for this version -func (v *version) AllMetadata() map[string]string { - return v.metadata -} diff --git a/identity/version_test.go b/identity/version_test.go deleted file mode 100644 index 385ad4d7..00000000 --- a/identity/version_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package identity - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/MichaelMure/git-bug/entity" - "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/lamport" -) - -func makeIdentityTestRepo(t *testing.T) repository.ClockedRepo { - repo := repository.NewMockRepo() - - clock1, err := repo.GetOrCreateClock("foo") - require.NoError(t, err) - err = clock1.Witness(42) - require.NoError(t, err) - - clock2, err := repo.GetOrCreateClock("bar") - require.NoError(t, err) - err = clock2.Witness(34) - require.NoError(t, err) - - return repo -} - -func TestVersionJSON(t *testing.T) { - repo := makeIdentityTestRepo(t) - - keys := []*Key{ - generatePublicKey(), - generatePublicKey(), - } - - before, err := newVersion(repo, "name", "email", "login", "avatarUrl", keys) - require.NoError(t, err) - - before.SetMetadata("key1", "value1") - before.SetMetadata("key2", "value2") - - expected := &version{ - id: entity.UnsetId, - name: "name", - email: "email", - login: "login", - avatarURL: "avatarUrl", - unixTime: time.Now().Unix(), - times: map[string]lamport.Time{ - "foo": 42, - "bar": 34, - }, - keys: keys, - nonce: before.nonce, - metadata: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - } - - require.Equal(t, expected, before) - - data, err := json.Marshal(before) - assert.NoError(t, err) - - var after version - err = json.Unmarshal(data, &after) - assert.NoError(t, err) - - // make sure we now have an Id - expected.Id() - - assert.Equal(t, expected, &after) -} -- cgit