Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Application Default Credentials for the google connector #2530

Merged
merged 11 commits into from
Sep 7, 2022
52 changes: 26 additions & 26 deletions connector/google/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 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)
nabokihms marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("error reading credentials from file: %v", err)
}
}
config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope)
if err != nil {
return nil, fmt.Errorf("unable to create directory service %v", err)
return nil, fmt.Errorf("unable to parse credentials to config: %v", err)
}
return srv, nil
config.Subject = email
return admin.NewService(ctx, option.WithHTTPClient(config.Client(ctx)))
}

// uniqueGroups returns the unique groups of a slice
Expand Down
145 changes: 145 additions & 0 deletions connector/google/google_test.go
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
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: "[email protected]",
ServiceAccountFilePath: serviceAccountFilePath,
},
expectedErr: "",
},
"adc": {
config: &Config{
ClientID: "testClient",
ClientSecret: "testSecret",
RedirectURI: ts.URL + "/callback",
Scopes: []string{"openid", "groups"},
AdminEmail: "[email protected]",
},
adc: serviceAccountFilePath,
expectedErr: "",
},
"adc_priority": {
config: &Config{
ClientID: "testClient",
ClientSecret: "testSecret",
RedirectURI: ts.URL + "/callback",
Scopes: []string{"openid", "groups"},
AdminEmail: "[email protected]",
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)
}
})
}
}