From 275dfc5d080dc392b00f033e735f22935c80dcc2 Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Sat, 11 Feb 2017 18:26:54 -0700 Subject: [PATCH] Support for Google Service Accounts Fixes #879. Allows using Google Service Accounts. To do so, initialize it with GSA credentials in JSON form: ```shell $ drive init -service-account-file ~/Desktop/gsaFile.json ``` Please make sure that you've enabled the Drive API and if you've enabled the API recently, wait a couple of minutes for the action to propagate through Google's systems, then retry. --- README.md | 12 +++++++ cmd/drive/main.go | 13 ++++++-- config/config.go | 4 +++ src/commands.go | 17 +++++++--- src/errors.go | 1 + src/help.go | 83 ++++++++++++++++++++++++----------------------- src/init.go | 33 +++++++++++++++++++ src/remote.go | 28 ++++++++++++++-- 8 files changed, 141 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 107ee8a5..7736a95c 100644 --- a/README.md +++ b/README.md @@ -173,11 +173,23 @@ A single hypen `-` can be used to specify options. However two hypens `--` can b Before you can use `drive`, you'll need to mount your Google Drive directory on your local file system: +#### OAuth2.0 credentials ```shell drive init ~/gdrive cd ~/gdrive ``` +#### Google Service Account credentials +```shell +drive init --service-account-file ~/gdrive +cd ~/gdrive +``` + +where must the GSA credentials in JSON form. +This feature was implemented as requested by: ++ https://github.com/odeke-em/drive/issues/879 + + ### De Initializing The opposite of `drive init`, it will remove your credentials locally as well as configuration associated files. diff --git a/cmd/drive/main.go b/cmd/drive/main.go index ef812a40..90d9d5b2 100644 --- a/cmd/drive/main.go +++ b/cmd/drive/main.go @@ -164,14 +164,23 @@ func (cmd *versionCmd) Run(args []string, definedFlags map[string]*flag.Flag) { exitWithError(nil) } -type initCmd struct{} +type initCmd struct { + ServiceAccountJSONFile *string `json:"-"` +} func (cmd *initCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { + cmd.ServiceAccountJSONFile = fs.String(drive.ServiceAccountJSONFileKey, "", "points the Google Service Account JSON file") return fs } func (cmd *initCmd) Run(args []string, definedFlags map[string]*flag.Flag) { - exitWithError(drive.New(initContext(args), nil).Init()) + comm := drive.New(initContext(args), nil) + gcsJSONFile := *cmd.ServiceAccountJSONFile + if gcsJSONFile == "" { + exitWithError(comm.Init()) + } else { + exitWithError(comm.InitWithServiceAccount(gcsJSONFile)) + } } type deInitCmd struct { diff --git a/config/config.go b/config/config.go index bb52f3fb..8a150445 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,8 @@ import ( "path/filepath" "strings" + "golang.org/x/oauth2/jwt" + "github.com/boltdb/bolt" ) @@ -49,6 +51,8 @@ const ( ) type Context struct { + GSAJWTConfig *jwt.Config `json:"gsa_jwt_config,omitempty"` + ClientId string `json:"client_id"` ClientSecret string `json:"client_secret"` RefreshToken string `json:"refresh_token"` diff --git a/src/commands.go b/src/commands.go index 0896db09..978caea1 100644 --- a/src/commands.go +++ b/src/commands.go @@ -16,6 +16,7 @@ package drive import ( "errors" + "fmt" "io" "os" "path" @@ -193,9 +194,17 @@ func (opts *Options) rcPath() (string, error) { } func New(context *config.Context, opts *Options) *Commands { - var r *Remote - if context != nil { - r = NewRemoteContext(context) + var rem *Remote + var err error + + if context.GSAJWTConfig != nil { + rem, err = NewRemoteContextFromServiceAccount(context.GSAJWTConfig) + } else { + rem, err = NewRemoteContext(context) + } + + if err != nil { + panic(fmt.Errorf("failed to initialize remoteContext: %v", err)) } stdin, stdout, stderr := os.Stdin, os.Stdout, os.Stderr @@ -236,7 +245,7 @@ func New(context *config.Context, opts *Options) *Commands { return &Commands{ context: context, - rem: r, + rem: rem, opts: opts, log: logger, mkdirAllCache: expirableCache.New(), diff --git a/src/errors.go b/src/errors.go index 81e73153..e827009b 100644 --- a/src/errors.go +++ b/src/errors.go @@ -41,6 +41,7 @@ const ( StatusNamedPipeReadAttempt ErrorStatus = 22 StatusContentTooLarge ErrorStatus = 23 StatusClashesFixed ErrorStatus = 24 + StatusSecurityException ErrorStatus = 25 ) type Error struct { diff --git a/src/help.go b/src/help.go index c1f7a31c..41eed900 100644 --- a/src/help.go +++ b/src/help.go @@ -25,47 +25,48 @@ import ( ) const ( - AboutKey = "about" - AllKey = "all" - CopyKey = "copy" - DeleteKey = "delete" - DeInitKey = "deinit" - EditDescriptionKey = "edit-description" - EditDescriptionShortKey = "edit-desc" - DiffKey = "diff" - AddressKey = "address" - EmptyTrashKey = "emptytrash" - FeaturesKey = "features" - HelpKey = "help" - InitKey = "init" - LinkKey = "Link" - ListKey = "list" - DuKey = "du" - Md5sumKey = "md5sum" - MoveKey = "move" - OcrKey = "ocr" - ConvertKey = "convert" - OSLinuxKey = "linux" - PullKey = "pull" - PipedKey = "piped" - PushKey = "push" - PubKey = "pub" - QRLinkKey = "qr" - RenameKey = "rename" - QuotaKey = "quota" - ShareKey = "share" - StatKey = "stat" - TouchKey = "touch" - TrashKey = "trash" - UnshareKey = "unshare" - UntrashKey = "untrash" - UnpubKey = "unpub" - VersionKey = "version" - NewKey = "new" - IndexKey = "index" - PruneKey = "prune" - StarKey = "star" - UnStarKey = "unstar" + AboutKey = "about" + AllKey = "all" + CopyKey = "copy" + DeleteKey = "delete" + DeInitKey = "deinit" + EditDescriptionKey = "edit-description" + EditDescriptionShortKey = "edit-desc" + ServiceAccountJSONFileKey = "service-account-file" + DiffKey = "diff" + AddressKey = "address" + EmptyTrashKey = "emptytrash" + FeaturesKey = "features" + HelpKey = "help" + InitKey = "init" + LinkKey = "Link" + ListKey = "list" + DuKey = "du" + Md5sumKey = "md5sum" + MoveKey = "move" + OcrKey = "ocr" + ConvertKey = "convert" + OSLinuxKey = "linux" + PullKey = "pull" + PipedKey = "piped" + PushKey = "push" + PubKey = "pub" + QRLinkKey = "qr" + RenameKey = "rename" + QuotaKey = "quota" + ShareKey = "share" + StatKey = "stat" + TouchKey = "touch" + TrashKey = "trash" + UnshareKey = "unshare" + UntrashKey = "untrash" + UnpubKey = "unpub" + VersionKey = "version" + NewKey = "new" + IndexKey = "index" + PruneKey = "prune" + StarKey = "star" + UnStarKey = "unstar" CoercedMimeKeyKey = "coerced-mime" ExportsKey = "export" diff --git a/src/init.go b/src/init.go index 6492c5a8..d423f0e7 100644 --- a/src/init.go +++ b/src/init.go @@ -15,9 +15,11 @@ package drive import ( + "io/ioutil" "os" "golang.org/x/net/context" + "golang.org/x/oauth2/google" ) func (g *Commands) Init() error { @@ -38,6 +40,37 @@ func (g *Commands) Init() error { return g.context.Write() } +// We don't need to perform an OAuth2.0 exchange +func (g *Commands) InitWithServiceAccount(gsaFilepath string) error { + if gsaFilepath == "" { + // We should be cautious enough not to blindly read in + // by default a Google Service account, because this + // could be a security breach to phish for accounts. + // The user has to explicitly pass in the path + return &Error{ + code: StatusSecurityException, + status: "a path has to be explicitly set for service accounts", + } + } + + blob, err := ioutil.ReadFile(gsaFilepath) + if err != nil { + return err + } + + jwtConfig, err := google.JWTConfigFromJSON(blob, DriveScope) + if err != nil { + return err + } + + // Next we'll just transfer the attributes directly + // by means of JSON marshaling the already vetted JWTConfig + g.context.GSAJWTConfig = jwtConfig + + // Since it validates alright, let's now write it to disk + return g.context.Write() +} + func (g *Commands) DeInit() error { prompt := func(args ...interface{}) bool { if !g.opts.canPrompt() { diff --git a/src/remote.go b/src/remote.go index 6653e62d..b39bd945 100644 --- a/src/remote.go +++ b/src/remote.go @@ -27,6 +27,7 @@ import ( "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" "github.com/odeke-em/drive/config" "github.com/odeke-em/statos" @@ -93,15 +94,36 @@ type Remote struct { progressChan chan int } -func NewRemoteContext(context *config.Context) *Remote { +// NewRemoteContextFromServiceAccount returns a remote initialized +// with credentials from a Google Service Account. +// For more information about these accounts, see: +// https://developers.google.com/identity/protocols/OAuth2ServiceAccount +// https://developers.google.com/accounts/docs/application-default-credentials +// +// You'll also need to configure access to Google Drive. +func NewRemoteContextFromServiceAccount(jwtConfig *jwt.Config) (*Remote, error) { + client := jwtConfig.Client(context.Background()) + return remoteFromClient(client) +} + +func NewRemoteContext(context *config.Context) (*Remote, error) { client := newOAuthClient(context) - service, _ := drive.New(client) + return remoteFromClient(client) +} + +func remoteFromClient(client *http.Client) (*Remote, error) { + service, err := drive.New(client) + if err != nil { + return nil, err + } + progressChan := make(chan int) - return &Remote{ + rem := &Remote{ progressChan: progressChan, service: service, client: client, } + return rem, nil } func hasExportLinks(f *File) bool {