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