Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Google Service Accounts #882

Merged
merged 1 commit into from
Feb 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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