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 }