mirror of
https://github.com/kopia/kopia.git
synced 2026-03-24 09:04:04 -04:00
Added support for reconnect tokens
Repository.Token() generates a base64-encoded token that can be stored in password manager that fully describes repository connection information (blob.ConnectionInfo) and optionally a password. Use `kopia repo status -t` to print the token. Use `kopia repo status -t -s` to print the token that also includes repository password. Use `kopia repo connect from-config --token T` to reconnect using the token.
This commit is contained in:
@@ -11,16 +11,29 @@
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
)
|
||||
|
||||
var connectToStorageFromConfigPath string
|
||||
var connectFromConfigFile string
|
||||
var connectFromConfigToken string
|
||||
|
||||
func connectToStorageFromConfig(ctx context.Context, isNew bool) (blob.Storage, error) {
|
||||
if isNew {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
|
||||
if connectFromConfigFile != "" {
|
||||
return connectToStorageFromConfigFile(ctx)
|
||||
}
|
||||
|
||||
if connectFromConfigToken != "" {
|
||||
return connectToStorageFromConfigToken(ctx)
|
||||
}
|
||||
|
||||
return nil, errors.New("either --file or --token must be provided")
|
||||
}
|
||||
|
||||
func connectToStorageFromConfigFile(ctx context.Context) (blob.Storage, error) {
|
||||
var cfg repo.LocalConfig
|
||||
|
||||
f, err := os.Open(connectToStorageFromConfigPath)
|
||||
f, err := os.Open(connectFromConfigFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to open config")
|
||||
}
|
||||
@@ -33,12 +46,23 @@ func connectToStorageFromConfig(ctx context.Context, isNew bool) (blob.Storage,
|
||||
return blob.NewStorage(ctx, cfg.Storage)
|
||||
}
|
||||
|
||||
func connectToStorageFromConfigToken(ctx context.Context) (blob.Storage, error) {
|
||||
ci, pass, err := repo.DecodeToken(connectFromConfigToken)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid token")
|
||||
}
|
||||
|
||||
passwordFromToken = pass
|
||||
return blob.NewStorage(ctx, ci)
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterStorageConnectFlags(
|
||||
"from-config",
|
||||
"the provided configuration file",
|
||||
func(cmd *kingpin.CmdClause) {
|
||||
cmd.Arg("file", "Path to the configuration file").Required().StringVar(&connectToStorageFromConfigPath)
|
||||
cmd.Flag("file", "Path to the configuration file").StringVar(&connectFromConfigFile)
|
||||
cmd.Flag("token", "Configuration token").StringVar(&connectFromConfigToken)
|
||||
},
|
||||
connectToStorageFromConfig)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
)
|
||||
|
||||
var (
|
||||
statusCommand = repositoryCommands.Command("status", "Display the status of connected repository.")
|
||||
statusCommand = repositoryCommands.Command("status", "Display the status of connected repository.")
|
||||
statusReconnectToken = statusCommand.Flag("reconnect-token", "Display reconnect command").Short('t').Bool()
|
||||
statusReconnectTokenIncludePassword = statusCommand.Flag("reconnect-token-with-password", "Include password in reconnect token").Short('s').Bool()
|
||||
)
|
||||
|
||||
func runStatusCommand(ctx context.Context, rep *repo.Repository) error {
|
||||
@@ -26,6 +28,7 @@ func runStatusCommand(ctx context.Context, rep *repo.Repository) error {
|
||||
if cjson, err := json.MarshalIndent(scrubber.ScrubSensitiveData(reflect.ValueOf(ci.Config)).Interface(), " ", " "); err == nil {
|
||||
fmt.Printf("Storage config: %v\n", string(cjson))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println()
|
||||
@@ -37,6 +40,24 @@ func runStatusCommand(ctx context.Context, rep *repo.Repository) error {
|
||||
fmt.Printf("Max pack length: %v\n", units.BytesStringBase2(int64(rep.Content.Format.MaxPackSize)))
|
||||
fmt.Printf("Splitter: %v\n", rep.Objects.Format.Splitter)
|
||||
|
||||
if *statusReconnectToken {
|
||||
pass := ""
|
||||
|
||||
if *statusReconnectTokenIncludePassword {
|
||||
pass = mustGetPasswordFromFlags(false, true)
|
||||
}
|
||||
|
||||
tok, err := rep.Token(pass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("\nTo reconnect to the repository use:\n\n$ kopia repository connect from-config --token %v\n\n", tok)
|
||||
if pass != "" {
|
||||
fmt.Printf("NOTICE: The token printed above can be trivially decoded to reveal the repository password. Do not store it in an unsecured place.\n")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,14 @@ func mustAskForExistingRepositoryPassword() string {
|
||||
return p1
|
||||
}
|
||||
|
||||
var passwordFromToken string
|
||||
|
||||
func mustGetPasswordFromFlags(isNew, allowPersistent bool) string {
|
||||
if passwordFromToken != "" {
|
||||
// password provided via token
|
||||
return passwordFromToken
|
||||
}
|
||||
|
||||
if !isNew && allowPersistent {
|
||||
pass, ok := getPersistedPassword(repositoryConfigFileName(), getUserName())
|
||||
if ok {
|
||||
|
||||
53
repo/token.go
Normal file
53
repo/token.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
)
|
||||
|
||||
type tokenInfo struct {
|
||||
Version string `json:"version"`
|
||||
Storage blob.ConnectionInfo `json:"storage"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// Token returns an opaque token that contains repository connection information
|
||||
// and optionally the provided password.
|
||||
func (r *Repository) Token(password string) (string, error) {
|
||||
ti := &tokenInfo{
|
||||
Version: "1",
|
||||
Storage: r.Blobs.ConnectionInfo(),
|
||||
Password: password,
|
||||
}
|
||||
|
||||
v, err := json.Marshal(ti)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "marshal token")
|
||||
}
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(v), nil
|
||||
}
|
||||
|
||||
// DecodeToken decodes the provided token and returns connection info and password if persisted.
|
||||
func DecodeToken(token string) (blob.ConnectionInfo, string, error) {
|
||||
t := &tokenInfo{}
|
||||
|
||||
v, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return blob.ConnectionInfo{}, "", errors.New("unable to decode token")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(v, t); err != nil {
|
||||
return blob.ConnectionInfo{}, "", errors.New("unable to decode token")
|
||||
}
|
||||
|
||||
if t.Version != "1" {
|
||||
return blob.ConnectionInfo{}, "", errors.New("unsupported token version")
|
||||
}
|
||||
|
||||
return t.Storage, t.Password, nil
|
||||
}
|
||||
@@ -36,6 +36,25 @@ To disconnect:
|
||||
$ kopia repository disconnect
|
||||
```
|
||||
|
||||
### Quick Reconnection To Repository
|
||||
|
||||
To quickly reconnect to the repository on another machine, you can use `kopia repository status -t`, which will print quick-reconnect command that encodes all repository connection parameters in an opaque token. You can also embed the repository password, by using `kopia repository status -t -s`.
|
||||
|
||||
Such command can be stored long-term in a secure location, such as password manager for easy recovery.
|
||||
|
||||
```shell
|
||||
$ kopia repository status -t -s
|
||||
...
|
||||
|
||||
To reconnect to the repository use:
|
||||
|
||||
$ kopia repository connect from-config --token 03Fy598cYIqbMlNNDz9VLU0K6Pk9alC...BNeazLBdRzP2MHo0MS83zRb
|
||||
|
||||
NOTICE: The token printed above can be trivially decoded to reveal the repository password. Do not store it in an unsecured place.
|
||||
```
|
||||
|
||||
> NOTE: Make sure to safeguard the repository token, as it gives full access to the repository to anybody in its posession.
|
||||
|
||||
### Configuration File
|
||||
|
||||
For each repository connection, Kopia maintains a configuration file and local cache:
|
||||
|
||||
@@ -121,6 +121,27 @@ func TestEndToEnd(t *testing.T) {
|
||||
e.runAndExpectSuccess(t, "repo", "status")
|
||||
})
|
||||
|
||||
t.Run("ReconnectUsingToken", func(t *testing.T) {
|
||||
lines := e.runAndExpectSuccess(t, "repo", "status", "-t", "-s")
|
||||
prefix := "$ kopia "
|
||||
var reconnectArgs []string
|
||||
|
||||
// look for output line containing the prefix - this will be our reconnect command
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, prefix) {
|
||||
reconnectArgs = strings.Split(strings.TrimPrefix(l, prefix), " ")
|
||||
}
|
||||
}
|
||||
|
||||
if reconnectArgs == nil {
|
||||
t.Fatalf("can't find reonnect command in kopia repo status output")
|
||||
}
|
||||
|
||||
e.runAndExpectSuccess(t, "repo", "disconnect")
|
||||
e.runAndExpectSuccess(t, reconnectArgs...)
|
||||
e.runAndExpectSuccess(t, "repo", "status")
|
||||
})
|
||||
|
||||
e.runAndExpectSuccess(t, "snapshot", "create", ".")
|
||||
e.runAndExpectSuccess(t, "snapshot", "list", ".")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user