Skip to content

Commit

Permalink
Support for Google Service Accounts
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odeke-em committed Feb 19, 2017
1 parent 7d3d7fa commit 275dfc5
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 50 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <gsa_json_file_path> ~/gdrive
cd ~/gdrive
```
where <gsa_json_file_path> 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.
Expand Down
13 changes: 11 additions & 2 deletions cmd/drive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"path/filepath"
"strings"

"golang.org/x/oauth2/jwt"

"github.com/boltdb/bolt"
)

Expand All @@ -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"`
Expand Down
17 changes: 13 additions & 4 deletions src/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package drive

import (
"errors"
"fmt"
"io"
"os"
"path"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions src/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const (
StatusNamedPipeReadAttempt ErrorStatus = 22
StatusContentTooLarge ErrorStatus = 23
StatusClashesFixed ErrorStatus = 24
StatusSecurityException ErrorStatus = 25
)

type Error struct {
Expand Down
83 changes: 42 additions & 41 deletions src/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 33 additions & 0 deletions src/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand Down
28 changes: 25 additions & 3 deletions src/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 275dfc5

Please sign in to comment.