Skip to content

Commit

Permalink
allow for injection of credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
mchaynes committed Jun 11, 2022
1 parent 0f95504 commit 3e47104
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 221 deletions.
212 changes: 6 additions & 206 deletions provider/pkg/internal/youtube/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,227 +2,27 @@ package youtube

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"

"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)

// This variable indicates whether the script should launch a web server to
// initiate the authorization flow or just display the URL in the terminal
// window. Note the following instructions based on this setting:
// * launchWebServer = true
// 1. Use OAuth2 credentials for a web application
// 2. Define authorized redirect URIs for the credential in the Google APIs
// Console and set the RedirectURL property on the config object to one
// of those redirect URIs. For example:
// config.RedirectURL = "http://localhost:8090"
// 3. In the startWebServer function below, update the URL in this line
// to match the redirect URI you selected:
// listener, err := net.Listen("tcp", "localhost:8090")
// The redirect URI identifies the URI to which the user is sent after
// completing the authorization flow. The listener then captures the
// authorization code in the URL and passes it back to this script.
// * launchWebServer = false
// 1. Use OAuth2 credentials for an installed application. (When choosing
// the application type for the OAuth2 client ID, select "Other".)
// 2. Set the redirect URI to "urn:ietf:wg:oauth:2.0:oob", like this:
// config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
// 3. When running the script, complete the auth flow. Then copy the
// authorization code from the browser and enter it on the command line.
const launchWebServer = true

const missingClientSecretsMessage = `
Please configure OAuth 2.0
To make this sample run, you need to populate the client_secrets.json file
found at:
%v
with information from the {{ Google Cloud Console }}
{{ https://cloud.google.com/console }}
For more information about the client_secrets.json file format, please visit:
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
`

// getClient uses a Context and Config to retrieve a Token
// then generate a Client. It returns the generated Client.
func getClient(scope string) *http.Client {
func getClient(scope, clientSecret, appCreds string) *http.Client {
ctx := context.Background()

s, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS")
b := []byte(s)
if !ok {
var err error
b, err = ioutil.ReadFile("client_secret.json")
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
}

// If modifying the scope, delete your previously saved credentials
// at ~/.credentials/youtube-go.json
config, err := google.ConfigFromJSON(b, scope)
config, err := google.ConfigFromJSON([]byte(clientSecret), scope)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}

// Use a redirect URI like this for a web app. The redirect URI must be a
// valid one for your OAuth2 credentials.
config.RedirectURL = "http://localhost:8090"
// Use the following redirect URI if launchWebServer=false in oauth2.go
// config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"

cacheFile, err := tokenCacheFile()
if err != nil {
log.Fatalf("Unable to get path to cached credential file. %v", err)
}
tok, err := tokenFromFile(cacheFile)
if err != nil {
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
if launchWebServer {
fmt.Println("Trying to get token from web")
tok, err = getTokenFromWeb(config, authURL)
} else {
fmt.Println("Trying to get token from prompt")
tok, err = getTokenFromPrompt(config, authURL)
}
if err == nil {
saveToken(cacheFile, tok)
}
}
return config.Client(ctx, tok)
}

// startWebServer starts a web server that listens on http://localhost:8080.
// The webserver waits for an oauth code in the three-legged auth flow.
func startWebServer() (codeCh chan string, err error) {
listener, err := net.Listen("tcp", "localhost:8090")
if err != nil {
return nil, err
}
codeCh = make(chan string)

go http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code := r.FormValue("code")
codeCh <- code // send code to OAuth flow
listener.Close()
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Received code: %v\r\nYou can now safely close this browser window.", code)
}))

return codeCh, nil
}

// openURL opens a browser window to the specified location.
// This code originally appeared at:
// http://stackoverflow.com/questions/10377243/how-can-i-launch-a-process-that-is-not-a-file-in-go
func openURL(url string) error {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", "http://localhost:4001/").Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("cannot open URL %s on this platform", url)
}
return err
}

// Exchange the authorization code for an access token
func exchangeToken(config *oauth2.Config, code string) (*oauth2.Token, error) {
tok, err := config.Exchange(context.Background(), code)
if err != nil {
log.Fatalf("Unable to retrieve token %v", err)
}
return tok, nil
}

// getTokenFromPrompt uses Config to request a Token and prompts the user
// to enter the token on the command line. It returns the retrieved Token.
func getTokenFromPrompt(config *oauth2.Config, authURL string) (*oauth2.Token, error) {
var code string
fmt.Printf("Go to the following link in your browser. After completing "+
"the authorization flow, enter the authorization code on the command "+
"line: \n%v\n", authURL)

if _, err := fmt.Scan(&code); err != nil {
log.Fatalf("Unable to read authorization code %v", err)
}
fmt.Println(authURL)
return exchangeToken(config, code)
}

// getTokenFromWeb uses Config to request a Token.
// It returns the retrieved Token.
func getTokenFromWeb(config *oauth2.Config, authURL string) (*oauth2.Token, error) {
codeCh, err := startWebServer()
if err != nil {
fmt.Printf("Unable to start a web server.")
return nil, err
}

err = openURL(authURL)
if err != nil {
log.Fatalf("Unable to open authorization URL in web server: %v", err)
} else {
fmt.Println("Your browser has been opened to an authorization URL.",
" This program will resume once authorization has been provided.")
fmt.Println(authURL)
}

// Wait for the web server to get the code.
code := <-codeCh
return exchangeToken(config, code)
}

// tokenCacheFile generates credential file path/filename.
// It returns the generated credential path/filename.
func tokenCacheFile() (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
os.MkdirAll(tokenCacheDir, 0700)
return filepath.Join(tokenCacheDir,
url.QueryEscape("youtube-go.json")), err
}

// tokenFromFile retrieves a Token from a given file path.
// It returns the retrieved Token and any read error encountered.
func tokenFromFile(file string) (*oauth2.Token, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
t := &oauth2.Token{}
err = json.NewDecoder(f).Decode(t)
defer f.Close()
return t, err
}

// saveToken uses a file path to create a file and store the
// token in it.
func saveToken(file string, token *oauth2.Token) {
fmt.Println("trying to save token")
fmt.Printf("Saving credential file to: %s\n", file)
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Unable to cache oauth token: %v", err)
if err = json.Unmarshal([]byte(appCreds), t); err != nil {
log.Fatalf("cannot parse token: %q %v", appCreds, err)
}
defer f.Close()
json.NewEncoder(f).Encode(token)

return config.Client(ctx, t)
}
16 changes: 2 additions & 14 deletions provider/pkg/internal/youtube/youtube.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package youtube

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
Expand All @@ -30,8 +28,8 @@ type PlaylistDiffResult struct {
Deletes []PlaylistItemDelete
}

func New(ctx context.Context) (*YouTube, error) {
client := getClient(youtube.YoutubeScope)
func New(ctx context.Context, clientSecret, appCreds string) (*YouTube, error) {
client := getClient(youtube.YoutubeScope, clientSecret, appCreds)
service, err := youtube.NewService(ctx, option.WithHTTPClient(client))
if err != nil {
return nil, fmt.Errorf("failed to create youtube service: %w", err)
Expand Down Expand Up @@ -213,13 +211,3 @@ func (y *YouTube) GetPlaylistItems(ctx context.Context, playlistId string) ([]*y
}
return items, nil
}

func mustPrettyPrint(f func() ([]byte, error)) {
data, err := f()
if err != nil {
panic(fmt.Errorf("failed to marshal data to json: %w", err))
}
buf := bytes.NewBuffer([]byte{})
json.Indent(buf, data, " ", " ")
fmt.Println(buf.String())
}
43 changes: 42 additions & 1 deletion provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"sort"
"strings"

"github.com/mchaynes/pulumi-yamltube/provider/pkg/internal/youtube"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
Expand Down Expand Up @@ -92,6 +94,7 @@ func (k *youtubeProvider) Construct(ctx context.Context, req *pulumirpc.Construc

// CheckConfig validates the configuration for this provider.
func (k *youtubeProvider) CheckConfig(ctx context.Context, req *pulumirpc.CheckRequest) (*pulumirpc.CheckResponse, error) {

return &pulumirpc.CheckResponse{Inputs: req.GetNews()}, nil
}

Expand All @@ -102,7 +105,11 @@ func (k *youtubeProvider) DiffConfig(ctx context.Context, req *pulumirpc.DiffReq

// Configure configures the resource provider with "globals" that control its behavior.
func (k *youtubeProvider) Configure(ctx context.Context, req *pulumirpc.ConfigureRequest) (*pulumirpc.ConfigureResponse, error) {
y, err := youtube.New(ctx)
ytc, err := ToConfig(req)
if err != nil {
return nil, err
}
y, err := youtube.New(ctx, ytc.GoogleClientSecret, ytc.GoogleAppCreds)
if err != nil {
return nil, fmt.Errorf("failed to connect to youtube: %w", err)
}
Expand Down Expand Up @@ -400,3 +407,37 @@ func MarshalPlaylist(p Playlist) (*structpb.Struct, error) {
plugin.MarshalOptions{KeepUnknowns: true, SkipNulls: true},
)
}

type YamlTubeConfig struct {
GoogleAppCreds string
GoogleClientSecret string
}

func ToConfig(req *pulumirpc.ConfigureRequest) (*YamlTubeConfig, error) {
config := make(map[string]string)

for key, val := range req.GetVariables() {
config[strings.TrimPrefix(key, "yamltube:config:")] = val
}

get := func(key, env string) (string, error) {
if val, ok := os.LookupEnv(env); ok {
return val, nil
}
if val, ok := config[key]; ok {
return val, nil
}
return "", fmt.Errorf("config %q not set nor is env var %q", key, env)
}

ytc := YamlTubeConfig{}
var err error
if ytc.GoogleAppCreds, err = get("google-application-credentials", "GOOGLE_APPLICATION_CREDENTIALS"); err != nil {
return nil, err
}

if ytc.GoogleClientSecret, err = get("google-client-secret", "GOOGLE_CLIENT_SECRET"); err != nil {
return nil, err
}
return &ytc, nil
}

0 comments on commit 3e47104

Please sign in to comment.