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")
|
scopes = append(scopes, "profile", "email")
|
||||||
}
|
}
|
||||||
|
|
||||||
srv, err := createDirectoryService(c.ServiceAccountFilePath, c.AdminEmail)
|
srv, err := createDirectoryService(c.ServiceAccountFilePath, c.AdminEmail, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, fmt.Errorf("could not create directory service: %v", err)
|
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
|
return uniqueGroups(userGroups), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createDirectoryService loads a google service account credentials file,
|
// createDirectoryService sets up super user impersonation and creates an admin client for calling
|
||||||
// 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
|
||||||
// the google admin api
|
// is used.
|
||||||
func createDirectoryService(serviceAccountFilePath string, email string) (*admin.Service, error) {
|
func createDirectoryService(serviceAccountFilePath, email string, logger log.Logger) (*admin.Service, error) {
|
||||||
if serviceAccountFilePath == "" && email == "" {
|
if email == "" {
|
||||||
return nil, nil
|
return nil, fmt.Errorf("directory service requires adminEmail")
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope)
|
var jsonCredentials []byte
|
||||||
if err != nil {
|
var err error
|
||||||
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
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client := config.Client(ctx)
|
if serviceAccountFilePath == "" {
|
||||||
|
logger.Warn("the application default credential is used since the service account file path is not used")
|
||||||
srv, err := admin.NewService(ctx, option.WithHTTPClient(client))
|
credential, err := google.FindDefaultCredentials(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to create directory service %v", err)
|
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
|
// 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