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