github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/registry.go (about)

     1  package lib
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"path/filepath"
     8  
     9  	"github.com/qri-io/qri/auth/key"
    10  	"github.com/qri-io/qri/base"
    11  	"github.com/qri-io/qri/config"
    12  	"github.com/qri-io/qri/event"
    13  	qhttp "github.com/qri-io/qri/lib/http"
    14  	"github.com/qri-io/qri/logbook"
    15  	"github.com/qri-io/qri/logbook/oplog"
    16  	"github.com/qri-io/qri/profile"
    17  	"github.com/qri-io/qri/registry"
    18  )
    19  
    20  // RegistryClientMethods defines business logic for working with registries
    21  type RegistryClientMethods struct {
    22  	d dispatcher
    23  }
    24  
    25  // Name returns the name of this method group
    26  func (m RegistryClientMethods) Name() string {
    27  	return "registry"
    28  }
    29  
    30  // Attributes defines attributes for each method
    31  func (m RegistryClientMethods) Attributes() map[string]AttributeSet {
    32  	return map[string]AttributeSet{
    33  		"createprofile":   {Endpoint: qhttp.DenyHTTP},
    34  		"proveprofilekey": {Endpoint: qhttp.DenyHTTP},
    35  	}
    36  }
    37  
    38  // RegistryProfile is a user profile as stored on a registry
    39  type RegistryProfile = registry.Profile
    40  
    41  // RegistryProfileParams encapsulates arguments for creating or proving a registry profile
    42  type RegistryProfileParams struct {
    43  	Profile *RegistryProfile
    44  }
    45  
    46  // CreateProfile creates a profile
    47  func (m RegistryClientMethods) CreateProfile(ctx context.Context, p *RegistryProfileParams) error {
    48  	_, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "createprofile"), p)
    49  	return dispatchReturnError(nil, err)
    50  }
    51  
    52  // ProveProfileKey sends proof to the registry that this user has control of a
    53  // specified private key, and modifies the user's config in order to reconcile
    54  // it with any already existing identity the registry knows about
    55  func (m RegistryClientMethods) ProveProfileKey(ctx context.Context, p *RegistryProfileParams) error {
    56  	_, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "proveprofilekey"), p)
    57  	return dispatchReturnError(nil, err)
    58  }
    59  
    60  // registryImpl holds the method implementations for RegistryMethods
    61  type registryImpl struct{}
    62  
    63  // CreateProfile creates a profile
    64  func (registryImpl) CreateProfile(scope scope, p *RegistryProfileParams) error {
    65  	pro, err := scope.RegistryClient().CreateProfile(p.Profile, scope.ActiveProfile().PrivKey)
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	err = scope.Bus().Publish(scope.Context(), event.ETRegistryProfileCreated, event.RegistryProfileCreated{
    71  		RegistryLocation: scope.Config().Registry.Location,
    72  		ProfileID:        pro.ProfileID,
    73  		Username:         pro.Username,
    74  	})
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	log.Debugf("create profile response: %v", pro)
    80  	*p.Profile = *pro
    81  
    82  	return updateConfig(scope, pro)
    83  }
    84  
    85  // ProveProfileKey sends proof to the registry that this user has control of a
    86  // specified private key, and modifies the user's config in order to reconcile
    87  // it with any already existing identity the registry knows about
    88  func (registryImpl) ProveProfileKey(scope scope, p *RegistryProfileParams) error {
    89  	// Check if the repository has any saved datasets. If so, calling prove is
    90  	// not allowed, because doing so would essentially throw away the old profile,
    91  	// making those references unreachable. In the future, this can be changed
    92  	// such that the old identity is given a different username, and is merged
    93  	// into the client's collection.
    94  	numRefs, err := scope.Repo().RefCount()
    95  	if err != nil {
    96  		return err
    97  	}
    98  	if numRefs > 0 {
    99  		return fmt.Errorf("cannot prove with a non-empty repository")
   100  	}
   101  
   102  	// For signing the outgoing message
   103  	privKey := scope.ActiveProfile().PrivKey
   104  
   105  	// set public key to send to server
   106  	pro := scope.ActiveProfile()
   107  	p.Profile.ProfileID = pro.ID.Encode()
   108  	p.Profile.PublicKey, err = key.EncodePubKeyB64(pro.PubKey)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// TODO(dustmop): Expand the signature to sign more than just the username
   114  	sigbytes, err := privKey.Sign([]byte(p.Profile.Username))
   115  	p.Profile.Signature = base64.StdEncoding.EncodeToString(sigbytes)
   116  
   117  	// Send proof to the registry
   118  	res, err := scope.RegistryClient().ProveKeyForProfile(p.Profile)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	log.Debugf("prove profile response: %v", res)
   123  
   124  	// Convert the profile to a configuration, assign the registry provided profileID
   125  	cfg := configFromProfile(scope, p.Profile)
   126  	if profileID, ok := res["profileID"]; ok {
   127  		// Store the old profile as the keyID since it came from the original keypair
   128  		cfg.Profile.KeyID = cfg.Profile.ID
   129  		cfg.Profile.ID = profileID
   130  		if pid := profile.IDB58DecodeOrEmpty(profileID); pid != "" {
   131  			// Assign to profile struct as well
   132  			pro.ID = pid
   133  		}
   134  	} else {
   135  		return fmt.Errorf("prove: server response invalid, did not have profileID")
   136  	}
   137  
   138  	// If an existing user of this profileID pushed something, get the previous logbook data
   139  	// and write it to our repository. This enables push and pull to continue to work
   140  	if logbookBin, ok := res["logbookInit"]; ok && len(logbookBin) > 0 {
   141  		logbookBytes, err := base64.StdEncoding.DecodeString(logbookBin)
   142  		if err != nil {
   143  			return err
   144  		}
   145  		lg := &oplog.Log{}
   146  		if err := lg.UnmarshalFlatbufferBytes(logbookBytes); err != nil {
   147  			return err
   148  		}
   149  		err = scope.Logbook().ReplaceAll(scope.Context(), lg)
   150  		if err != nil {
   151  			return err
   152  		}
   153  	} else {
   154  		// Otherwise, nothing was ever pushed. Create new logbook data using the
   155  		// profileID we got back.
   156  		logbookPath := filepath.Join(scope.RepoPath(), "logbook.qfb")
   157  		logbook, err := logbook.NewJournalOverwriteWithProfile(*pro, scope.Bus(),
   158  			scope.Filesystem(), logbookPath)
   159  		if err != nil {
   160  			return err
   161  		}
   162  		scope.SetLogbook(logbook)
   163  	}
   164  
   165  	// Save the modified config
   166  	return scope.ChangeConfig(cfg)
   167  }
   168  
   169  // Construct a config with the same values as the profile
   170  func configFromProfile(scope scope, pro *registry.Profile) *config.Config {
   171  	cfg := scope.Config().Copy()
   172  	cfg.Profile.Peername = pro.Username
   173  	cfg.Profile.Created = pro.Created
   174  	cfg.Profile.Email = pro.Email
   175  	cfg.Profile.Photo = pro.Photo
   176  	cfg.Profile.Thumb = pro.Thumb
   177  	cfg.Profile.Name = pro.Name
   178  	cfg.Profile.Description = pro.Description
   179  	cfg.Profile.HomeURL = pro.HomeURL
   180  	cfg.Profile.Twitter = pro.Twitter
   181  
   182  	return cfg
   183  }
   184  
   185  func updateConfig(scope scope, pro *registry.Profile) error {
   186  	cfg := configFromProfile(scope, pro)
   187  
   188  	// TODO (b5) - this should be automatically done by inst.ChangeConfig
   189  	repoPro, err := profile.NewProfile(cfg.Profile)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	// TODO (b5) - this is the lowest level place I could find to monitor for
   195  	// profile name changes, not sure this makes the most sense to have this here.
   196  	// we should consider a separate track for any change that affects the peername,
   197  	// it should always be verified by any set registry before saving
   198  	if cfg.Profile.Peername != scope.ActiveProfile().Peername {
   199  		if err := base.ModifyRepoUsername(scope.Context(), scope.Repo(), scope.Logbook(), scope.ActiveProfile().Peername, cfg.Profile.Peername); err != nil {
   200  			return err
   201  		}
   202  	}
   203  
   204  	if err := scope.Profiles().SetOwner(scope.Context(), repoPro); err != nil {
   205  		return err
   206  	}
   207  
   208  	return scope.ChangeConfig(cfg)
   209  }