Skip to content
Open
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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ Once tokens have been obtained, `zat -no-server` will perform only archival duti
### Credentials

* Obtain Google credentials

There are two options: the interactive OAuth flow (default), or Application Default Credentials.

**Option A: Application Default Credentials (ADC)**

Run `zat -google-adc` to authenticate with [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials)
instead of `google.config.json`/`google.creds.json`. There is no interactive login flow and no creds files to manage.
Credentials are discovered, in order, from:
1. the `GOOGLE_APPLICATION_CREDENTIALS` environment variable (path to a service account key)
2. the gcloud-managed user credentials file
3. the GCP metadata server (when running on GCE, Cloud Run, etc.)

For local user credentials, grant the Drive scope when logging in:
```
gcloud auth application-default login --scopes=https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/cloud-platform
```

Note: a service account can only access Drive folders shared with it (or its own Drive) unless domain-wide delegation is configured.

**Option B: interactive OAuth (default)**

* [Create an Oauth Client ID credential](https://console.cloud.google.com/apis/credentials)
* You may need to create a project, or use a dev project you have access to. If the project doesn't have OAuth consent screen info, you'll need to add that as well.
* Choose "internal" user type, give it a name similar to the project name, and add your contact email
Expand Down Expand Up @@ -61,7 +82,7 @@ Once tokens have been obtained, `zat -no-server` will perform only archival duti
}
```

Once the credentials are in place, re-run `zat` and use the web server at http://localhost:8080/ to login to both Google and Zoom to create the `*.creds.json` files zat will use for the next run.
Once the credentials are in place, re-run `zat` and use the web server at http://localhost:8080/ to login to both Google and Zoom to create the `*.creds.json` files zat will use for the next run. (When using `-google-adc`, the Google web login is skipped — only Zoom needs the interactive flow.)

### Configuration

Expand Down
19 changes: 14 additions & 5 deletions cmd/google/findfolders/findfolders.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,23 @@ import (
func main() {
andQuery := flag.String("query", "", "google drive query: https://developers.google.com/drive/api/v3/search-files")
cfgDir := cmd.FlagConfigDir()
googleADC := flag.Bool("google-adc", false,
"authenticate to Google with Application Default Credentials instead of "+
cmd.GoogleConfigPath+"/"+cmd.GoogleCredsPath)
flag.Parse()

logger := log.New(os.Stderr, "", cmd.LogFmt)
googleClient, err := google.NewClientFromFile(
logger,
path.Join(*cfgDir, cmd.GoogleConfigPath),
google.NewCredentialsManager(cmd.GoogleCredsPath).ClientOption,
)
var googleClient *google.Client
var err error
if *googleADC {
googleClient, err = google.NewClientFromADC(context.TODO(), logger)
} else {
googleClient, err = google.NewClientFromFile(
logger,
path.Join(*cfgDir, cmd.GoogleConfigPath),
google.NewCredentialsManager(cmd.GoogleCredsPath).ClientOption,
)
}
if err != nil {
logger.Fatal(err)
}
Expand Down
50 changes: 50 additions & 0 deletions google/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ type Client struct {
logger *log.Logger
httpClient *http.Client

// cloud creds
config *oauth2.Config
credentials *oauth2.Token
cm *credentialsManager

// ADC
tokenSource oauth2.TokenSource
}

type ClientOption func(*Client)
Expand Down Expand Up @@ -67,6 +71,29 @@ func NewClientFromReader(logger *log.Logger, r io.Reader, options ...ClientOptio
return NewClient(logger, config, options...)
}

// NewClientFromADC builds a Client that authenticates with Application Default
// Credentials. Credentials are discovered from the GOOGLE_APPLICATION_CREDENTIALS
// environment variable, the gcloud-managed ADC file, or the GCP metadata server.
// No google.config.json / google.creds.json files are needed and the interactive
// OAuth flow is skipped.
func NewClientFromADC(ctx context.Context, logger *log.Logger, options ...ClientOption) (*Client, error) {
creds, err := google.FindDefaultCredentials(ctx, drive.DriveScope)
if err != nil {
return nil, err
}
c := &Client{
logger: logger,
httpClient: http.DefaultClient,
tokenSource: creds.TokenSource,
}

for _, o := range options {
o(c)
}
return c, nil
}

// NewClient builds a Client that authenticates with Google Cloud Credentials
func NewClient(logger *log.Logger, config *oauth2.Config, options ...ClientOption) (*Client, error) {
c := &Client{
logger: logger,
Expand All @@ -90,6 +117,16 @@ func (c *Client) updateCreds(token *oauth2.Token) {
}

func (c *Client) HasCreds() bool {
if c.tokenSource != nil {
// ADC: the token source handles fetching/refreshing. A successful
// Token() call confirms credentials are available and valid.
if _, err := c.tokenSource.Token(); err != nil {
c.logger.Printf("error getting ADC google token %s", err)
return false
}
return true
}

if c.credentials == nil {
return false
}
Expand All @@ -114,13 +151,23 @@ func (c *Client) HasCreds() bool {
}

func (c *Client) OauthRedirect(w http.ResponseWriter, r *http.Request) {
if c.tokenSource != nil {
// ADC mode has no interactive OAuth flow.
http.Redirect(w, r, "/", http.StatusFound)
return
}
c.logger.Println(c.config.RedirectURL)
http.Redirect(w, r, c.config.RedirectURL, http.StatusFound)
}

// TODO: use / validate state token
func (c *Client) OauthHandler() func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if c.tokenSource != nil {
// ADC mode has no interactive OAuth flow.
http.Redirect(w, r, "/", http.StatusFound)
return
}
if r.FormValue("refresh") != "" || !c.credentials.Valid() {
c.updateCreds(nil)
}
Expand All @@ -147,6 +194,9 @@ func (c *Client) OauthHandler() func(w http.ResponseWriter, r *http.Request) {
}

func (c *Client) Service(ctx context.Context) (*drive.Service, error) {
if c.tokenSource != nil {
return drive.NewService(ctx, option.WithTokenSource(c.tokenSource))
}
return drive.NewService(ctx, option.WithTokenSource(c.config.TokenSource(ctx, c.credentials)))
}

Expand Down
19 changes: 14 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,9 @@ func main() {
uploadFilter := flag.String("t", "",
"comma separated list of file types to archive (mp4, m4a, timeline, transcript, chat, cc, csv), see: "+
"https://marketplace.zoom.us/docs/api-reference/zoom-api/cloud-recording/recordingget")
googleADC := flag.Bool("google-adc", false,
"authenticate to Google with Application Default Credentials instead of "+
cmd.GoogleConfigPath+"/"+cmd.GoogleCredsPath)
flag.Parse()

logger := log.New(os.Stderr, "", cmd.LogFmt)
Expand All @@ -583,11 +586,17 @@ func main() {
http.DefaultClient = apmhttp.WrapClient(http.DefaultClient)
http.DefaultTransport = apmhttp.WrapRoundTripper(http.DefaultTransport)

googleClient, err := google.NewClientFromFile(
logger,
path.Join(*cfgDir, cmd.GoogleConfigPath),
google.NewCredentialsManager(path.Join(*cfgDir, cmd.GoogleCredsPath)).ClientOption,
)
var err error
var googleClient *google.Client
if *googleADC {
googleClient, err = google.NewClientFromADC(context.Background(), logger)
} else {
googleClient, err = google.NewClientFromFile(
logger,
path.Join(*cfgDir, cmd.GoogleConfigPath),
google.NewCredentialsManager(path.Join(*cfgDir, cmd.GoogleCredsPath)).ClientOption,
)
}
if err != nil {
logger.Fatal(err)
}
Expand Down