Implement Application Default Credentials for the google connector (#2530)
Signed-off-by: Trung <trung.hoang@pricehubble.com>
This commit is contained in:
parent
cbe3d24587
commit
a1a3ed5b25
@ -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