diff --git a/docs/content/drive.md b/docs/content/drive.md index 3ac874a2e..ba10800d7 100644 --- a/docs/content/drive.md +++ b/docs/content/drive.md @@ -65,6 +65,8 @@ Google Application Client Id - leave blank normally. client_id> Google Application Client Secret - leave blank normally. client_secret> +Service Account Credentials JSON file path - needed only if you want use SA instead of interactive login. +service_account_file> Remote config Use auto config? * Say Y if not sure @@ -113,6 +115,25 @@ To copy a local directory to a drive directory called backup rclone copy /home/source remote:backup +### Service Account support ### + +You can set up rclone with Google Drive in an unattended mode, +i.e. not tied to a specific end-user Google account. This is useful +when you want to synchronise files onto machines that don't have +actively logged-in users, for example build machines. + +To create a service account and obtain its credentials, go to the +[Google Developer Console](https://console.developers.google.com) and +use the "Create Credentials" button. After creating an account, a JSON +file containing the Service Account's credentials will be downloaded +onto your machine. These credentials are what rclone will use for +authentication. + +To use a Service Account instead of OAuth2 token flow, enter the path +to your Service Account credentials at the `service_account_file` +prompt and rclone won't use the browser based authentication +flow. + ### Team drives ### If you want to configure the remote to point to a Google Team Drive diff --git a/drive/drive.go b/drive/drive.go index f8a7e1689..3adb2a73a 100644 --- a/drive/drive.go +++ b/drive/drive.go @@ -10,8 +10,10 @@ package drive import ( "fmt" "io" + "io/ioutil" "log" "net/http" + "os" "path" "sort" "strconv" @@ -96,9 +98,12 @@ func init() { Description: "Google Drive", NewFs: NewFs, Config: func(name string) { - err := oauthutil.Config("drive", name, driveConfig) - if err != nil { - log.Fatalf("Failed to configure token: %v", err) + var err error + if fs.ConfigFileGet(name, "service_account_file") == "" { + err = oauthutil.Config("drive", name, driveConfig) + if err != nil { + log.Fatalf("Failed to configure token: %v", err) + } } err = configTeamDrive(name) if err != nil { @@ -111,6 +116,9 @@ func init() { }, { Name: fs.ConfigClientSecret, Help: "Google Application Client Secret - leave blank normally.", + }, { + Name: "service_account_file", + Help: "Service Account Credentials JSON file path - needed only if you want use SA instead of interactive login.", }}, }) fs.VarP(&driveUploadCutoff, "drive-upload-cutoff", "", "Cutoff for switching to chunked upload") @@ -338,9 +346,9 @@ func configTeamDrive(name string) error { if !fs.Confirm() { return nil } - client, _, err := oauthutil.NewClient(name, driveConfig) + client, err := authenticate(name) if err != nil { - return errors.Wrap(err, "config team drive failed to make oauth client") + return errors.Wrap(err, "config team drive failed to authenticate") } svc, err := drive.New(client) if err != nil { @@ -382,6 +390,39 @@ func newPacer() *pacer.Pacer { return pacer.New().SetMinSleep(minSleep).SetPacer(pacer.GoogleDrivePacer) } +func getServiceAccountClient(keyJsonfilePath string) (*http.Client, error) { + data, err := ioutil.ReadFile(os.ExpandEnv(keyJsonfilePath)) + if err != nil { + return nil, errors.Wrap(err, "error opening credentials file") + } + conf, err := google.JWTConfigFromJSON(data, driveConfig.Scopes...) + if err != nil { + return nil, errors.Wrap(err, "error processing credentials") + } + ctxWithSpecialClient := oauthutil.Context(fs.Config.Client()) + return oauth2.NewClient(ctxWithSpecialClient, conf.TokenSource(ctxWithSpecialClient)), nil +} + +func authenticate(name string) (*http.Client, error) { + var oAuthClient *http.Client + var err error + + serviceAccountPath := fs.ConfigFileGet(name, "service_account_file") + if serviceAccountPath != "" { + oAuthClient, err = getServiceAccountClient(serviceAccountPath) + if err != nil { + return nil, errors.Wrap(err, "Failed to configure drive Service Account") + } + } else { + oAuthClient, _, err = oauthutil.NewClient(name, driveConfig) + if err != nil { + return nil, errors.Wrap(err, "Failed to configure drive") + } + } + + return oAuthClient, nil +} + // NewFs contstructs an Fs from the path, container:path func NewFs(name, path string) (fs.Fs, error) { if !isPowerOfTwo(int64(chunkSize)) { @@ -391,10 +432,7 @@ func NewFs(name, path string) (fs.Fs, error) { return nil, errors.Errorf("drive: chunk size can't be less than 256k - was %v", chunkSize) } - oAuthClient, _, err := oauthutil.NewClient(name, driveConfig) - if err != nil { - log.Fatalf("Failed to configure drive: %v", err) - } + oAuthClient, _ := authenticate(name) root, err := parseDrivePath(path) if err != nil {