identity.go

  1// Package identity contains the identity data model and low-level related functions
  2package identity
  3
  4import (
  5	"encoding/json"
  6	"fmt"
  7	"os"
  8	"reflect"
  9	"strings"
 10	"time"
 11
 12	"github.com/pkg/errors"
 13
 14	"github.com/MichaelMure/git-bug/entity"
 15	"github.com/MichaelMure/git-bug/repository"
 16	"github.com/MichaelMure/git-bug/util/git"
 17	"github.com/MichaelMure/git-bug/util/lamport"
 18	"github.com/MichaelMure/git-bug/util/timestamp"
 19)
 20
 21const identityRefPattern = "refs/identities/"
 22const identityRemoteRefPattern = "refs/remotes/%s/identities/"
 23const versionEntryName = "version"
 24const identityConfigKey = "git-bug.identity"
 25
 26var ErrNonFastForwardMerge = errors.New("non fast-forward identity merge")
 27var ErrNoIdentitySet = errors.New("No identity is set.\n" +
 28	"To interact with bugs, an identity first needs to be created using " +
 29	"\"git bug user create\"")
 30var ErrMultipleIdentitiesSet = errors.New("multiple user identities set")
 31
 32var _ Interface = &Identity{}
 33var _ entity.Interface = &Identity{}
 34
 35type Identity struct {
 36	// Id used as unique identifier
 37	id entity.Id
 38
 39	// all the successive version of the identity
 40	versions []*Version
 41
 42	// not serialized
 43	lastCommit git.Hash
 44}
 45
 46func NewIdentity(name string, email string) *Identity {
 47	return &Identity{
 48		id: entity.UnsetId,
 49		versions: []*Version{
 50			{
 51				name:  name,
 52				email: email,
 53				nonce: makeNonce(20),
 54			},
 55		},
 56	}
 57}
 58
 59func NewIdentityFull(name string, email string, avatarUrl string) *Identity {
 60	return &Identity{
 61		id: entity.UnsetId,
 62		versions: []*Version{
 63			{
 64				name:      name,
 65				email:     email,
 66				avatarURL: avatarUrl,
 67				nonce:     makeNonce(20),
 68			},
 69		},
 70	}
 71}
 72
 73// MarshalJSON will only serialize the id
 74func (i *Identity) MarshalJSON() ([]byte, error) {
 75	return json.Marshal(&IdentityStub{
 76		id: i.id,
 77	})
 78}
 79
 80// UnmarshalJSON will only read the id
 81// Users of this package are expected to run Load() to load
 82// the remaining data from the identities data in git.
 83func (i *Identity) UnmarshalJSON(data []byte) error {
 84	panic("identity should be loaded with identity.UnmarshalJSON")
 85}
 86
 87// ReadLocal load a local Identity from the identities data available in git
 88func ReadLocal(repo repository.Repo, id entity.Id) (*Identity, error) {
 89	ref := fmt.Sprintf("%s%s", identityRefPattern, id)
 90	return read(repo, ref)
 91}
 92
 93// ReadRemote load a remote Identity from the identities data available in git
 94func ReadRemote(repo repository.Repo, remote string, id string) (*Identity, error) {
 95	ref := fmt.Sprintf(identityRemoteRefPattern, remote) + id
 96	return read(repo, ref)
 97}
 98
 99// read will load and parse an identity from git
100func read(repo repository.Repo, ref string) (*Identity, error) {
101	refSplit := strings.Split(ref, "/")
102	id := entity.Id(refSplit[len(refSplit)-1])
103
104	if err := id.Validate(); err != nil {
105		return nil, errors.Wrap(err, "invalid ref")
106	}
107
108	hashes, err := repo.ListCommits(ref)
109
110	// TODO: this is not perfect, it might be a command invoke error
111	if err != nil {
112		return nil, ErrIdentityNotExist
113	}
114
115	i := &Identity{
116		id: id,
117	}
118
119	for _, hash := range hashes {
120		entries, err := repo.ListEntries(hash)
121		if err != nil {
122			return nil, errors.Wrap(err, "can't list git tree entries")
123		}
124
125		if len(entries) != 1 {
126			return nil, fmt.Errorf("invalid identity data at hash %s", hash)
127		}
128
129		entry := entries[0]
130
131		if entry.Name != versionEntryName {
132			return nil, fmt.Errorf("invalid identity data at hash %s", hash)
133		}
134
135		data, err := repo.ReadData(entry.Hash)
136		if err != nil {
137			return nil, errors.Wrap(err, "failed to read git blob data")
138		}
139
140		var version Version
141		err = json.Unmarshal(data, &version)
142
143		if err != nil {
144			return nil, errors.Wrapf(err, "failed to decode Identity version json %s", hash)
145		}
146
147		// tag the version with the commit hash
148		version.commitHash = hash
149		i.lastCommit = hash
150
151		i.versions = append(i.versions, &version)
152	}
153
154	return i, nil
155}
156
157type StreamedIdentity struct {
158	Identity *Identity
159	Err      error
160}
161
162// ReadAllLocalIdentities read and parse all local Identity
163func ReadAllLocalIdentities(repo repository.ClockedRepo) <-chan StreamedIdentity {
164	return readAllIdentities(repo, identityRefPattern)
165}
166
167// ReadAllRemoteIdentities read and parse all remote Identity for a given remote
168func ReadAllRemoteIdentities(repo repository.ClockedRepo, remote string) <-chan StreamedIdentity {
169	refPrefix := fmt.Sprintf(identityRemoteRefPattern, remote)
170	return readAllIdentities(repo, refPrefix)
171}
172
173// Read and parse all available bug with a given ref prefix
174func readAllIdentities(repo repository.ClockedRepo, refPrefix string) <-chan StreamedIdentity {
175	out := make(chan StreamedIdentity)
176
177	go func() {
178		defer close(out)
179
180		refs, err := repo.ListRefs(refPrefix)
181		if err != nil {
182			out <- StreamedIdentity{Err: err}
183			return
184		}
185
186		for _, ref := range refs {
187			b, err := read(repo, ref)
188
189			if err != nil {
190				out <- StreamedIdentity{Err: err}
191				return
192			}
193
194			out <- StreamedIdentity{Identity: b}
195		}
196	}()
197
198	return out
199}
200
201// NewFromGitUser will query the repository for user detail and
202// build the corresponding Identity
203func NewFromGitUser(repo repository.Repo) (*Identity, error) {
204	name, err := repo.GetUserName()
205	if err != nil {
206		return nil, err
207	}
208	if name == "" {
209		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.name \"John Doe\"`")
210	}
211
212	email, err := repo.GetUserEmail()
213	if err != nil {
214		return nil, err
215	}
216	if email == "" {
217		return nil, errors.New("user name is not configured in git yet. Please use `git config --global user.email johndoe@example.com`")
218	}
219
220	return NewIdentity(name, email), nil
221}
222
223// SetUserIdentity store the user identity's id in the git config
224func SetUserIdentity(repo repository.RepoConfig, identity *Identity) error {
225	return repo.LocalConfig().StoreString(identityConfigKey, identity.Id().String())
226}
227
228// GetUserIdentity read the current user identity, set with a git config entry
229func GetUserIdentity(repo repository.Repo) (*Identity, error) {
230	id, err := GetUserIdentityId(repo)
231	if err != nil {
232		return nil, err
233	}
234
235	i, err := ReadLocal(repo, id)
236	if err == ErrIdentityNotExist {
237		innerErr := repo.LocalConfig().RemoveAll(identityConfigKey)
238		if innerErr != nil {
239			_, _ = fmt.Fprintln(os.Stderr, errors.Wrap(innerErr, "can't clear user identity").Error())
240		}
241		return nil, err
242	}
243
244	return i, nil
245}
246
247func GetUserIdentityId(repo repository.Repo) (entity.Id, error) {
248	configs, err := repo.LocalConfig().ReadAll(identityConfigKey)
249	if err != nil {
250		return entity.UnsetId, err
251	}
252
253	if len(configs) == 0 {
254		return entity.UnsetId, ErrNoIdentitySet
255	}
256
257	if len(configs) > 1 {
258		return entity.UnsetId, ErrMultipleIdentitiesSet
259	}
260
261	var id entity.Id
262	for _, val := range configs {
263		id = entity.Id(val)
264	}
265
266	if err := id.Validate(); err != nil {
267		return entity.UnsetId, err
268	}
269
270	return id, nil
271}
272
273// IsUserIdentitySet say if the user has set his identity
274func IsUserIdentitySet(repo repository.Repo) (bool, error) {
275	configs, err := repo.LocalConfig().ReadAll(identityConfigKey)
276	if err != nil {
277		return false, err
278	}
279
280	return len(configs) == 1, nil
281}
282
283type Mutator struct {
284	Name      string
285	Email     string
286	AvatarUrl string
287	Keys      []*Key
288}
289
290// Mutate allow to create a new version of the Identity
291func (i *Identity) Mutate(f func(orig Mutator) Mutator) {
292	orig := Mutator{
293		Name:      i.Name(),
294		Email:     i.Email(),
295		AvatarUrl: i.AvatarUrl(),
296		Keys:      i.Keys(),
297	}
298	mutated := f(orig)
299	if reflect.DeepEqual(orig, mutated) {
300		return
301	}
302	i.versions = append(i.versions, &Version{
303		name:      mutated.Name,
304		email:     mutated.Email,
305		avatarURL: mutated.AvatarUrl,
306		keys:      mutated.Keys,
307	})
308}
309
310// Write the identity into the Repository. In particular, this ensure that
311// the Id is properly set.
312func (i *Identity) Commit(repo repository.ClockedRepo) error {
313	// Todo: check for mismatch between memory and commit data
314
315	if !i.NeedCommit() {
316		return fmt.Errorf("can't commit an identity with no pending version")
317	}
318
319	if err := i.Validate(); err != nil {
320		return errors.Wrap(err, "can't commit an identity with invalid data")
321	}
322
323	for _, v := range i.versions {
324		if v.commitHash != "" {
325			i.lastCommit = v.commitHash
326			// ignore already commit versions
327			continue
328		}
329
330		// get the times where new versions starts to be valid
331		v.time = repo.EditTime()
332		v.unixTime = time.Now().Unix()
333
334		blobHash, err := v.Write(repo)
335		if err != nil {
336			return err
337		}
338
339		// Make a git tree referencing the blob
340		tree := []repository.TreeEntry{
341			{ObjectType: repository.Blob, Hash: blobHash, Name: versionEntryName},
342		}
343
344		treeHash, err := repo.StoreTree(tree)
345		if err != nil {
346			return err
347		}
348
349		var commitHash git.Hash
350		if i.lastCommit != "" {
351			commitHash, err = repo.StoreCommitWithParent(treeHash, i.lastCommit)
352		} else {
353			commitHash, err = repo.StoreCommit(treeHash)
354		}
355
356		if err != nil {
357			return err
358		}
359
360		i.lastCommit = commitHash
361		v.commitHash = commitHash
362
363		// if it was the first commit, use the commit hash as the Identity id
364		if i.id == "" || i.id == entity.UnsetId {
365			i.id = entity.Id(commitHash)
366		}
367	}
368
369	if i.id == "" {
370		panic("identity with no id")
371	}
372
373	ref := fmt.Sprintf("%s%s", identityRefPattern, i.id)
374	err := repo.UpdateRef(ref, i.lastCommit)
375
376	if err != nil {
377		return err
378	}
379
380	return nil
381}
382
383func (i *Identity) CommitAsNeeded(repo repository.ClockedRepo) error {
384	if !i.NeedCommit() {
385		return nil
386	}
387	return i.Commit(repo)
388}
389
390func (i *Identity) NeedCommit() bool {
391	for _, v := range i.versions {
392		if v.commitHash == "" {
393			return true
394		}
395	}
396
397	return false
398}
399
400// Merge will merge a different version of the same Identity
401//
402// To make sure that an Identity history can't be altered, a strict fast-forward
403// only policy is applied here. As an Identity should be tied to a single user, this
404// should work in practice but it does leave a possibility that a user would edit his
405// Identity from two different repo concurrently and push the changes in a non-centralized
406// network of repositories. In this case, it would result in some of the repo accepting one
407// version and some other accepting another, preventing the network in general to converge
408// to the same result. This would create a sort of partition of the network, and manual
409// cleaning would be required.
410//
411// An alternative approach would be to have a determinist rebase:
412// - any commits present in both local and remote version would be kept, never changed.
413// - newer commits would be merged in a linear chain of commits, ordered based on the
414//   Lamport time
415//
416// However, this approach leave the possibility, in the case of a compromised crypto keys,
417// of forging a new version with a bogus Lamport time to be inserted before a legit version,
418// invalidating the correct version and hijacking the Identity. There would only be a short
419// period of time where this would be possible (before the network converge) but I'm not
420// confident enough to implement that. I choose the strict fast-forward only approach,
421// despite it's potential problem with two different version as mentioned above.
422func (i *Identity) Merge(repo repository.Repo, other *Identity) (bool, error) {
423	if i.id != other.id {
424		return false, errors.New("merging unrelated identities is not supported")
425	}
426
427	if i.lastCommit == "" || other.lastCommit == "" {
428		return false, errors.New("can't merge identities that has never been stored")
429	}
430
431	modified := false
432	for j, otherVersion := range other.versions {
433		// if there is more version in other, take them
434		if len(i.versions) == j {
435			i.versions = append(i.versions, otherVersion)
436			i.lastCommit = otherVersion.commitHash
437			modified = true
438		}
439
440		// we have a non fast-forward merge.
441		// as explained in the doc above, refusing to merge
442		if i.versions[j].commitHash != otherVersion.commitHash {
443			return false, ErrNonFastForwardMerge
444		}
445	}
446
447	if modified {
448		err := repo.UpdateRef(identityRefPattern+i.id.String(), i.lastCommit)
449		if err != nil {
450			return false, err
451		}
452	}
453
454	return false, nil
455}
456
457// Validate check if the Identity data is valid
458func (i *Identity) Validate() error {
459	lastTime := lamport.Time(0)
460
461	if len(i.versions) == 0 {
462		return fmt.Errorf("no version")
463	}
464
465	for _, v := range i.versions {
466		if err := v.Validate(); err != nil {
467			return err
468		}
469
470		if v.commitHash != "" && v.time < lastTime {
471			return fmt.Errorf("non-chronological version (%d --> %d)", lastTime, v.time)
472		}
473
474		lastTime = v.time
475	}
476
477	// The identity Id should be the hash of the first commit
478	if i.versions[0].commitHash != "" && string(i.versions[0].commitHash) != i.id.String() {
479		return fmt.Errorf("identity id should be the first commit hash")
480	}
481
482	return nil
483}
484
485func (i *Identity) lastVersion() *Version {
486	if len(i.versions) <= 0 {
487		panic("no version at all")
488	}
489
490	return i.versions[len(i.versions)-1]
491}
492
493// Id return the Identity identifier
494func (i *Identity) Id() entity.Id {
495	if i.id == "" {
496		// simply panic as it would be a coding error
497		// (using an id of an identity not stored yet)
498		panic("no id yet")
499	}
500	return i.id
501}
502
503// Name return the last version of the name
504func (i *Identity) Name() string {
505	return i.lastVersion().name
506}
507
508// Email return the last version of the email
509func (i *Identity) Email() string {
510	return i.lastVersion().email
511}
512
513// AvatarUrl return the last version of the Avatar URL
514func (i *Identity) AvatarUrl() string {
515	return i.lastVersion().avatarURL
516}
517
518// Keys return the last version of the valid keys
519func (i *Identity) Keys() []*Key {
520	return i.lastVersion().keys
521}
522
523// ValidKeysAtTime return the set of keys valid at a given lamport time
524func (i *Identity) ValidKeysAtTime(time lamport.Time) []*Key {
525	var result []*Key
526
527	for _, v := range i.versions {
528		if v.time > time {
529			return result
530		}
531
532		result = v.keys
533	}
534
535	return result
536}
537
538// DisplayName return a non-empty string to display, representing the
539// identity, based on the non-empty values.
540func (i *Identity) DisplayName() string {
541	return i.Name()
542}
543
544// IsProtected return true if the chain of git commits started to be signed.
545// If that's the case, only signed commit with a valid key for this identity can be added.
546func (i *Identity) IsProtected() bool {
547	// Todo
548	return false
549}
550
551// LastModificationLamportTime return the Lamport time at which the last version of the identity became valid.
552func (i *Identity) LastModificationLamport() lamport.Time {
553	return i.lastVersion().time
554}
555
556// LastModification return the timestamp at which the last version of the identity became valid.
557func (i *Identity) LastModification() timestamp.Timestamp {
558	return timestamp.Timestamp(i.lastVersion().unixTime)
559}
560
561// SetMetadata store arbitrary metadata along the last not-commit Version.
562// If the Version has been commit to git already, a new identical version is added and will need to be
563// commit.
564func (i *Identity) SetMetadata(key string, value string) {
565	if i.lastVersion().commitHash != "" {
566		i.versions = append(i.versions, i.lastVersion().Clone())
567	}
568	i.lastVersion().SetMetadata(key, value)
569}
570
571// ImmutableMetadata return all metadata for this Identity, accumulated from each Version.
572// If multiple value are found, the first defined takes precedence.
573func (i *Identity) ImmutableMetadata() map[string]string {
574	metadata := make(map[string]string)
575
576	for _, version := range i.versions {
577		for key, value := range version.metadata {
578			if _, has := metadata[key]; !has {
579				metadata[key] = value
580			}
581		}
582	}
583
584	return metadata
585}
586
587// MutableMetadata return all metadata for this Identity, accumulated from each Version.
588// If multiple value are found, the last defined takes precedence.
589func (i *Identity) MutableMetadata() map[string]string {
590	metadata := make(map[string]string)
591
592	for _, version := range i.versions {
593		for key, value := range version.metadata {
594			metadata[key] = value
595		}
596	}
597
598	return metadata
599}
600
601// addVersionForTest add a new version to the identity
602// Only for testing !
603func (i *Identity) addVersionForTest(version *Version) {
604	i.versions = append(i.versions, version)
605}