Implement Application Default Credentials for the google connector (#2530)
Signed-off-by: Trung <trung.hoang@pricehubble.com>
This commit is contained in:
		| @@ -71,7 +71,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e | ||||
| 		scopes = append(scopes, "profile", "email") | ||||
| 	} | ||||
|  | ||||
| 	srv, err := createDirectoryService(c.ServiceAccountFilePath, c.AdminEmail) | ||||
| 	srv, err := createDirectoryService(c.ServiceAccountFilePath, c.AdminEmail, logger) | ||||
| 	if err != nil { | ||||
| 		cancel() | ||||
| 		return nil, fmt.Errorf("could not create directory service: %v", err) | ||||
| @@ -279,37 +279,37 @@ func (c *googleConnector) getGroups(email string, fetchTransitiveGroupMembership | ||||
| 	return uniqueGroups(userGroups), nil | ||||
| } | ||||
|  | ||||
| // createDirectoryService loads a google service account credentials file, | ||||
| // sets up super user impersonation and creates an admin client for calling | ||||
| // the google admin api | ||||
| func createDirectoryService(serviceAccountFilePath string, email string) (*admin.Service, error) { | ||||
| 	if serviceAccountFilePath == "" && email == "" { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if serviceAccountFilePath == "" || email == "" { | ||||
| 		return nil, fmt.Errorf("directory service requires both serviceAccountFilePath and adminEmail") | ||||
| 	} | ||||
| 	jsonCredentials, err := os.ReadFile(serviceAccountFilePath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error reading credentials from file: %v", err) | ||||
| // createDirectoryService sets up super user impersonation and creates an admin client for calling | ||||
| // the google admin api. If no serviceAccountFilePath is defined, the application default credential | ||||
| // is used. | ||||
| func createDirectoryService(serviceAccountFilePath, email string, logger log.Logger) (*admin.Service, error) { | ||||
| 	if email == "" { | ||||
| 		return nil, fmt.Errorf("directory service requires adminEmail") | ||||
| 	} | ||||
|  | ||||
| 	config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("unable to parse client secret file to config: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Impersonate an admin. This is mandatory for the admin APIs. | ||||
| 	config.Subject = email | ||||
| 	var jsonCredentials []byte | ||||
| 	var err error | ||||
|  | ||||
| 	ctx := context.Background() | ||||
| 	client := config.Client(ctx) | ||||
|  | ||||
| 	srv, err := admin.NewService(ctx, option.WithHTTPClient(client)) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("unable to create directory service %v", err) | ||||
| 	if serviceAccountFilePath == "" { | ||||
| 		logger.Warn("the application default credential is used since the service account file path is not used") | ||||
| 		credential, err := google.FindDefaultCredentials(ctx) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed to fetch application default credentials: %w", err) | ||||
| 		} | ||||
| 		jsonCredentials = credential.JSON | ||||
| 	} else { | ||||
| 		jsonCredentials, err = os.ReadFile(serviceAccountFilePath) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error reading credentials from file: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return srv, nil | ||||
| 	config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("unable to parse credentials to config: %v", err) | ||||
| 	} | ||||
| 	config.Subject = email | ||||
| 	return admin.NewService(ctx, option.WithHTTPClient(config.Client(ctx))) | ||||
| } | ||||
|  | ||||
| // uniqueGroups returns the unique groups of a slice | ||||
|   | ||||
							
								
								
									
										145
									
								
								connector/google/google_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								connector/google/google_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| package google | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func testSetup(t *testing.T) *httptest.Server { | ||||
| 	mux := http.NewServeMux() | ||||
| 	// TODO: mock calls | ||||
| 	// mux.HandleFunc("/admin/directory/v1/groups", func(w http.ResponseWriter, r *http.Request) { | ||||
| 	// 	w.Header().Add("Content-Type", "application/json") | ||||
| 	// 	json.NewEncoder(w).Encode(&admin.Groups{ | ||||
| 	// 		Groups: []*admin.Group{}, | ||||
| 	// 	}) | ||||
| 	// }) | ||||
| 	return httptest.NewServer(mux) | ||||
| } | ||||
|  | ||||
| func newConnector(config *Config, serverURL string) (*googleConnector, error) { | ||||
| 	log := logrus.New() | ||||
| 	conn, err := config.Open("id", log) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	googleConn, ok := conn.(*googleConnector) | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("failed to convert to googleConnector") | ||||
| 	} | ||||
| 	return googleConn, nil | ||||
| } | ||||
|  | ||||
| func tempServiceAccountKey() (string, error) { | ||||
| 	fd, err := os.CreateTemp("", "google_service_account_key") | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer fd.Close() | ||||
| 	err = json.NewEncoder(fd).Encode(map[string]string{ | ||||
| 		"type":                 "service_account", | ||||
| 		"project_id":           "sample-project", | ||||
| 		"private_key_id":       "sample-key-id", | ||||
| 		"private_key":          "-----BEGIN PRIVATE KEY-----\nsample-key\n-----END PRIVATE KEY-----\n", | ||||
| 		"client_id":            "sample-client-id", | ||||
| 		"client_x509_cert_url": "localhost", | ||||
| 	}) | ||||
| 	return fd.Name(), err | ||||
| } | ||||
|  | ||||
| func TestOpen(t *testing.T) { | ||||
| 	ts := testSetup(t) | ||||
| 	defer ts.Close() | ||||
|  | ||||
| 	type testCase struct { | ||||
| 		config      *Config | ||||
| 		expectedErr string | ||||
|  | ||||
| 		// string to set in GOOGLE_APPLICATION_CREDENTIALS. As local development environments can | ||||
| 		// already contain ADC, test cases will be built uppon this setting this env variable | ||||
| 		adc string | ||||
| 	} | ||||
|  | ||||
| 	serviceAccountFilePath, err := tempServiceAccountKey() | ||||
| 	assert.Nil(t, err) | ||||
|  | ||||
| 	for name, reference := range map[string]testCase{ | ||||
| 		"missing_admin_email": { | ||||
| 			config: &Config{ | ||||
| 				ClientID:     "testClient", | ||||
| 				ClientSecret: "testSecret", | ||||
| 				RedirectURI:  ts.URL + "/callback", | ||||
| 				Scopes:       []string{"openid", "groups"}, | ||||
| 			}, | ||||
| 			expectedErr: "requires adminEmail", | ||||
| 		}, | ||||
| 		"service_account_key_not_found": { | ||||
| 			config: &Config{ | ||||
| 				ClientID:               "testClient", | ||||
| 				ClientSecret:           "testSecret", | ||||
| 				RedirectURI:            ts.URL + "/callback", | ||||
| 				Scopes:                 []string{"openid", "groups"}, | ||||
| 				AdminEmail:             "foo@bar.com", | ||||
| 				ServiceAccountFilePath: "not_found.json", | ||||
| 			}, | ||||
| 			expectedErr: "error reading credentials", | ||||
| 		}, | ||||
| 		"service_account_key_valid": { | ||||
| 			config: &Config{ | ||||
| 				ClientID:               "testClient", | ||||
| 				ClientSecret:           "testSecret", | ||||
| 				RedirectURI:            ts.URL + "/callback", | ||||
| 				Scopes:                 []string{"openid", "groups"}, | ||||
| 				AdminEmail:             "foo@bar.com", | ||||
| 				ServiceAccountFilePath: serviceAccountFilePath, | ||||
| 			}, | ||||
| 			expectedErr: "", | ||||
| 		}, | ||||
| 		"adc": { | ||||
| 			config: &Config{ | ||||
| 				ClientID:     "testClient", | ||||
| 				ClientSecret: "testSecret", | ||||
| 				RedirectURI:  ts.URL + "/callback", | ||||
| 				Scopes:       []string{"openid", "groups"}, | ||||
| 				AdminEmail:   "foo@bar.com", | ||||
| 			}, | ||||
| 			adc:         serviceAccountFilePath, | ||||
| 			expectedErr: "", | ||||
| 		}, | ||||
| 		"adc_priority": { | ||||
| 			config: &Config{ | ||||
| 				ClientID:               "testClient", | ||||
| 				ClientSecret:           "testSecret", | ||||
| 				RedirectURI:            ts.URL + "/callback", | ||||
| 				Scopes:                 []string{"openid", "groups"}, | ||||
| 				AdminEmail:             "foo@bar.com", | ||||
| 				ServiceAccountFilePath: serviceAccountFilePath, | ||||
| 			}, | ||||
| 			adc:         "/dev/null", | ||||
| 			expectedErr: "", | ||||
| 		}, | ||||
| 	} { | ||||
| 		reference := reference | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			assert := assert.New(t) | ||||
|  | ||||
| 			os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", reference.adc) | ||||
| 			conn, err := newConnector(reference.config, ts.URL) | ||||
|  | ||||
| 			if reference.expectedErr == "" { | ||||
| 				assert.Nil(err) | ||||
| 				assert.NotNil(conn) | ||||
| 			} else { | ||||
| 				assert.ErrorContains(err, reference.expectedErr) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user