From 046ce0b71bb2696e0aef28c59f9ac63ec0289dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20D=C3=B6ll?= Date: Mon, 20 Jul 2020 15:39:49 +0200 Subject: [PATCH] (refactor) use service account --- README.md | 70 +++++++------------- cmd/google.go | 39 ----------- cmd/lambda.go | 2 +- cmd/root.go | 14 ++-- internal/config/config.go | 32 ++++----- internal/config/config_test.go | 4 +- internal/google/auth.go | 116 --------------------------------- internal/google/client.go | 32 ++++++--- internal/sync.go | 15 ++--- 9 files changed, 74 insertions(+), 250 deletions(-) delete mode 100644 cmd/google.go delete mode 100644 internal/google/auth.go diff --git a/README.md b/README.md index 61549aad..fbab7dcc 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ for regular synchronization. ## Configuration You need a few items of configuration. One side from AWS, and the other -from Google Cloud / Apps to allow for API access to each. You should have configured +from Google Cloud to allow for API access to each. You should have configured Google as your Identity Provider for AWS SSO already. You will need the files produced by these steps for AWS Lambda deployment as well @@ -49,40 +49,15 @@ as locally running the ssosync tool. ### Google -Head to the [Google Cloud Console](https://console.cloud.google.com/) for your Domain -(Specifically API & Services -> -[Credentials](https://console.cloud.google.com/projectselector2/apis/credentials)) -and Create a Project. +First, you have to setup your API. In the project you want to use go to the [Console](https://console.developers.google.com/apis) and select *API & Services* > *Enable APIs and Services*. Search for *Admin SDK* and *Enable* the API. -Creating a project will take a few seconds. Once it is complete, you can then Configure the Consent -Screen (there will be a clear warning and button for it). Click Through and select "Internal". Give -a name and press Save as you don't need the rest. +You have to perform this [tutorial](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) to create a service account that you use to sync your users. Save the JSON file your create during the process and rename it to `credentials.json`. -Now go back to Credentials, Click Create Credentials and then select OAuth client ID. Select Other and -provide a name. You will be displayed credentials, press okay and then use the download button, and a -JSON file will download. +> you can also use the `--google-credentials` parameter to explicitly specify the file with the service credentials. Please, keep this file safe, or store it in the AWS Secrets Manager -**THIS FILE IS IMPORTANT AND SECRET - KEEP IT SAFE** +In the domain-wide delegation for the Admin API, you have to specificy the following scopes for user. -With this done, you can log in and generate a token.json file. To create the file, use the -`ssosync google` command. With help output, it looks like this: - -```text -Log in to Google - use me to generate the files needed for the main command - -Usage: - ssosync google [flags] - -Flags: - -h, --help help for google - --path string set the path to find credentials (default "credentials.json") - --tokenPath string set the path to put token.json output into (default "token.json") -``` - -When you run the command correctly, it will give a URL to load in your browser. Go to it, and you'll get -a string to paste back and enter. Once you paste the line in, the file generates. - -The Token file is useless without the Credentials File - but keep it safe. +`https://www.googleapis.com/auth/admin.directory.group.readonly,https://www.googleapis.com/auth/admin.directory.group.member.readonly,https://www.googleapis.com/auth/admin.directory.user.readonly` Back in the Console go to the Dashboard for the API & Services and select "Enable API and Services". In the Search box type `Admin` and select the `Admin SDK` option. Click the `Enable` button. @@ -93,12 +68,13 @@ Go to the AWS Single Sign-On console in the region you have set up AWS SSO and s Settings. Click `Enable automatic provisioning`. A pop up will appear with URL and the Access Token. The Access Token will only appear -at this stage. You want to copy both of these into a text file which ends in the extension -`.toml`. +at this stage. You want to copy both of these as a parameter to the `ssosync` command. + +Or you specifc these as environment variables. -```toml -Token = "tokenHere" -Endpoint = "https://scim.eu-west-1.amazonaws.com/a-guid-would-be-here/scim/v2/" +``` +SSOSYNC_SCIM_ACCESS_TOKEN= +SSOSYNC_SCIM_ENDPOINT= ``` ## Local Usage @@ -110,23 +86,21 @@ The default for ssosync is to run through the sync. ```text A command line tool to enable you to synchronise your Google Apps (G-Suite) users to AWS Single Sign-on (AWS SSO) +Complete documentation is available at https://github.com/awslabs/ssosync Usage: ssosync [flags] - ssosync [command] - -Available Commands: - google Log in to Google - help Help about any command Flags: - -d, --debug Enable verbose / debug logging - -c, --googleCredentialsPath string set the path to find credentials for Google (default "credentials.json") - -t, --googleTokenPath string set the path to find token for Google (default "token.json") - -h, --help help for ssosync - -s, --scimConfig string AWS SSO SCIM Configuration (default "aws.toml") - -Use "ssosync [command] --help" for more information about a command. + -t, --access-token string SCIM Access Token + -d, --debug Enable verbose / debug logging + -e, --endpoint string SCIM Endpoint + -u, --google-admin string Google Admin Email + -c, --google-credentials string set the path to find credentials for Google (default "credentials.json") + -h, --help help for ssosync + --log-format string log format (default "text") + --log-level string log level (default "warn") + -v, --version version for ssosync ``` The output of the command when run without 'debug' turned on looks like this: diff --git a/cmd/google.go b/cmd/google.go deleted file mode 100644 index 7512bb17..00000000 --- a/cmd/google.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2020, Amazon.com, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "github.com/spf13/cobra" - - "github.com/awslabs/ssosync/internal/google" -) - -var googleCmd = &cobra.Command{ - Use: "google", - Short: "Log in to Google", - Long: `Log in to Google - use me to generate the files needed for the main command`, - RunE: func(cmd *cobra.Command, args []string) error { - g, err := google.NewAuthClient(cfg.GoogleCredentialsPath, cfg.GoogleTokenPath) - if err != nil { - return err - } - - if _, err := g.GetTokenFromWeb(); err != nil { - return err - } - - return nil - }, -} diff --git a/cmd/lambda.go b/cmd/lambda.go index d233cbf5..e675151c 100644 --- a/cmd/lambda.go +++ b/cmd/lambda.go @@ -85,7 +85,7 @@ func removeFileSilently(name string) { _ = os.Remove(name) } -// lambdaHandler is the Lambda entry point +// lambdaHandler is the Lambda entry point. func lambdaHandler(cfg *config.Config) func() error { return func() error { if err := internal.DoSync(cfg); err != nil { diff --git a/cmd/root.go b/cmd/root.go index e2f60c22..2ff1e495 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -72,11 +72,9 @@ func init() { // initialize cobra cobra.OnInitialize(initConfig) - addFlags(rootCmd, cfg) rootCmd.SetVersionTemplate(fmt.Sprintf("%s, commit %s, built at %s by %s\n", version, commit, date, builtBy)) - rootCmd.AddCommand(googleCmd) // silence on the root cmd rootCmd.SilenceUsage = true @@ -90,8 +88,8 @@ func initConfig() { viper.AutomaticEnv() viper.BindEnv("google_credentials") - viper.BindEnv("google_token") - viper.BindEnv("aws_toml") + viper.BindEnv("scim_access_token") + viper.BindEnv("scim_endpoint") viper.BindEnv("log_level") viper.BindEnv("log_format") @@ -104,12 +102,14 @@ func initConfig() { } func addFlags(cmd *cobra.Command, cfg *config.Config) { - rootCmd.PersistentFlags().StringVarP(&cfg.GoogleCredentialsPath, "googleCredentialsPath", "c", config.DefaultGoogleCredentialsPath, "set the path to find credentials for Google") - rootCmd.PersistentFlags().StringVarP(&cfg.GoogleTokenPath, "googleTokenPath", "t", config.DefaultGoogleTokenPath, "set the path to find token for Google") + rootCmd.PersistentFlags().StringVarP(&cfg.GoogleCredentials, "google-admin", "a", config.DefaultGoogleCredentials, "set the path to find credentials for Google") rootCmd.PersistentFlags().BoolVarP(&cfg.Debug, "debug", "d", config.DefaultDebug, "Enable verbose / debug logging") rootCmd.PersistentFlags().StringVarP(&cfg.LogFormat, "log-format", "", config.DefaultLogFormat, "log format") rootCmd.PersistentFlags().StringVarP(&cfg.LogLevel, "log-level", "", config.DefaultLogLevel, "log level") - rootCmd.Flags().StringVarP(&cfg.SCIMConfig, "scimConfig", "s", config.DefaultSCIMConfig, "AWS SSO SCIM Configuration") + rootCmd.Flags().StringVarP(&cfg.SCIMAccessToken, "access-token", "t", "", "SCIM Access Token") + rootCmd.Flags().StringVarP(&cfg.SCIMEndpoint, "endpoint", "e", "", "SCIM Endpoint") + rootCmd.Flags().StringVarP(&cfg.GoogleCredentials, "google-credentials", "c", config.DefaultGoogleCredentials, "set the path to find credentials for Google") + rootCmd.Flags().StringVarP(&cfg.GoogleAdmin, "google-admin", "u", "", "Google Admin Email") } func logConfig(cfg *config.Config) { diff --git a/internal/config/config.go b/internal/config/config.go index 6e49b3a1..fb3dcf31 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,12 +8,14 @@ type Config struct { LogLevel string `mapstructure:"log_level"` // LogFormat is the format that is used for logging LogFormat string `mapstructure:"log_format"` - // GoogleCredentialsPath is the path to the credentials - GoogleCredentialsPath string `mapstructure:"google_credentials"` - // GoogleTokenPath is the path to the token - GoogleTokenPath string `mapstructure:"google_token"` - // SCIMConfig is the path to the AWS SSO SCIM Config - SCIMConfig string `mapstructure:"aws_toml"` + // GoogleCredentials ... + GoogleCredentials string `mapstructure:"google_credentials"` + // GoogleAdmin ... + GoogleAdmin string `mapstructure:"google_admin"` + // SCIMEndpoint .... + SCIMEndpoint string `mapstructure:"scim_endpoint"` + // SCIMAccessToken ... + SCIMAccessToken string `mapstructure:"scim_access_token"` } const ( @@ -23,22 +25,16 @@ const ( DefaultLogFormat = "text" // DefaultDebug is the default debug status. DefaultDebug = false - // DefaultGoogleCredentialsPath is the default credentials path - DefaultGoogleCredentialsPath = "credentials.json" - // DefaultGoogleTokenPath is the default token path - DefaultGoogleTokenPath = "token.json" - // DefaultSCIMConfig is the default for the AWS SSO SCIM Configuraiton - DefaultSCIMConfig = "aws.toml" + // DefaultGoogleCredentials is the default credentials path + DefaultGoogleCredentials = "credentials.json" ) // New returns a new Config func New() *Config { return &Config{ - Debug: DefaultDebug, - LogLevel: DefaultLogLevel, - LogFormat: DefaultLogFormat, - GoogleCredentialsPath: DefaultGoogleCredentialsPath, - GoogleTokenPath: DefaultGoogleTokenPath, - SCIMConfig: DefaultSCIMConfig, + Debug: DefaultDebug, + LogLevel: DefaultLogLevel, + LogFormat: DefaultLogFormat, + GoogleCredentials: DefaultGoogleCredentials, } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2c144d2f..6794c369 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -18,7 +18,5 @@ func TestConfig(t *testing.T) { assert.Equal(cfg.LogLevel, DefaultLogLevel) assert.Equal(cfg.LogFormat, DefaultLogFormat) assert.Equal(cfg.Debug, DefaultDebug) - assert.Equal(cfg.GoogleCredentialsPath, DefaultGoogleCredentialsPath) - assert.Equal(cfg.GoogleTokenPath, DefaultGoogleTokenPath) - assert.Equal(cfg.SCIMConfig, DefaultSCIMConfig) + assert.Equal(cfg.GoogleCredentials, DefaultGoogleCredentials) } diff --git a/internal/google/auth.go b/internal/google/auth.go deleted file mode 100644 index 95d365a8..00000000 --- a/internal/google/auth.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2020, Amazon.com, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package google - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - admin "google.golang.org/api/admin/directory/v1" -) - -// AuthClient is for authenticating with Google and optionally -// getting a token from the web interface interactively -type AuthClient struct { - credentialsPath string - tokenPath string - - config *oauth2.Config -} - -// NewAuthClient creates a new AuthClient with the paths given -func NewAuthClient(credPath string, tokenPath string) (*AuthClient, error) { - b, err := ioutil.ReadFile(credPath) - if err != nil { - return nil, err - } - - config, err := google.ConfigFromJSON(b, - admin.AdminDirectoryGroupReadonlyScope, - admin.AdminDirectoryGroupMemberReadonlyScope, - admin.AdminDirectoryUserReadonlyScope, - ) - - if err != nil { - return nil, err - } - - return &AuthClient{ - credentialsPath: credPath, - tokenPath: tokenPath, - config: config, - }, nil -} - -// GetClient will return the http.Client for the authenticated connection -func (a *AuthClient) GetClient() (*http.Client, error) { - tok, err := tokenFromFile(a.tokenPath) - if err != nil { - return nil, err - } - return a.config.Client(context.Background(), tok), nil -} - -// GetTokenFromWeb will interactively get a token from Google -func (a *AuthClient) GetTokenFromWeb() (*oauth2.Token, error) { - authURL := a.config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) - fmt.Printf("Go to the following link in your browser then type the "+ - "authorization code: \n%v\nAuth Code: ", authURL) - - var authCode string - if _, err := fmt.Scan(&authCode); err != nil { - return nil, err - } - - tok, err := a.config.Exchange(context.TODO(), authCode) - if err != nil { - return nil, err - } - - a.saveToken(tok) - - return tok, nil -} - -// saveToken will save the token to the token.json file -func (a *AuthClient) saveToken(token *oauth2.Token) error { - fmt.Printf("Saving credential file to: %s\n", a.tokenPath) - f, err := os.OpenFile(a.tokenPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer f.Close() - _ = json.NewEncoder(f).Encode(token) - - return nil -} - -// tokenFromFile retrieves a token from a local file. -func tokenFromFile(file string) (*oauth2.Token, error) { - f, err := os.Open(file) - if err != nil { - return nil, err - } - defer f.Close() - tok := &oauth2.Token{} - err = json.NewDecoder(f).Decode(tok) - return tok, err -} diff --git a/internal/google/client.go b/internal/google/client.go index 2776553d..133e5f54 100644 --- a/internal/google/client.go +++ b/internal/google/client.go @@ -16,8 +16,9 @@ package google import ( "context" - "net/http" + "fmt" + "golang.org/x/oauth2/google" admin "google.golang.org/api/admin/directory/v1" "google.golang.org/api/option" ) @@ -30,24 +31,33 @@ type Client interface { } type client struct { - client *http.Client + ctx context.Context service *admin.Service } // NewClient creates a new client for Google's Admin API -func NewClient(auth *AuthClient) (Client, error) { - c, err := auth.GetClient() +func NewClient(adminEmail string, serviceAccountKey []byte) (Client, error) { + ctx := context.Background() + + config, err := google.JWTConfigFromJSON(serviceAccountKey, admin.AdminDirectoryGroupReadonlyScope, + admin.AdminDirectoryGroupMemberReadonlyScope, + admin.AdminDirectoryUserReadonlyScope) + + config.Subject = adminEmail + if err != nil { return nil, err } - srv, err := admin.NewService(context.TODO(), option.WithHTTPClient(c)) + ts := config.TokenSource(ctx) + + srv, err := admin.NewService(ctx, option.WithTokenSource(ts)) if err != nil { return nil, err } return &client{ - client: c, + ctx: ctx, service: srv, }, nil } @@ -55,12 +65,14 @@ func NewClient(auth *AuthClient) (Client, error) { // GetUsers will get the users from Google's Admin API func (c *client) GetUsers() (u []*admin.User, err error) { u = make([]*admin.User, 0) - err = c.service.Users.List().Customer("my_customer").Pages(context.TODO(), func(users *admin.Users) error { + err = c.service.Users.List().Customer("my_customer").Pages(c.ctx, func(users *admin.Users) error { u = append(u, users.Users...) return nil }) - return + fmt.Println(err) + + return u, err } // GetGroups will get the groups from Google's Admin API @@ -71,7 +83,7 @@ func (c *client) GetGroups() (g []*admin.Group, err error) { return nil }) - return + return g, err } // GetGroupMembers will get the members of the group specified @@ -82,5 +94,5 @@ func (c *client) GetGroupMembers(g *admin.Group) (m []*admin.Member, err error) return nil }) - return + return m, err } diff --git a/internal/sync.go b/internal/sync.go index 9866f1eb..cc7f5fe1 100644 --- a/internal/sync.go +++ b/internal/sync.go @@ -15,6 +15,7 @@ package internal import ( + "io/ioutil" "net/http" "github.com/awslabs/ssosync/internal/aws" @@ -209,24 +210,22 @@ func (s *SyncGSuite) SyncGroups() error { func DoSync(cfg *config.Config) error { log.Info("Creating the Google and AWS Clients needed") - googleAuthClient, err := google.NewAuthClient(cfg.GoogleCredentialsPath, cfg.GoogleTokenPath) + b, err := ioutil.ReadFile(cfg.GoogleCredentials) if err != nil { return err } - googleClient, err := google.NewClient(googleAuthClient) - if err != nil { - return err - } - - awsConfig, err := aws.ReadConfigFromFile(cfg.SCIMConfig) + googleClient, err := google.NewClient(cfg.GoogleAdmin, b) if err != nil { return err } awsClient, err := aws.NewClient( &http.Client{}, - awsConfig) + &aws.Config{ + Endpoint: cfg.SCIMEndpoint, + Token: cfg.SCIMAccessToken, + }) if err != nil { return err }