Skip to content

Commit

Permalink
Implement Application Default Credentials for the google connector (#…
Browse files Browse the repository at this point in the history
…2530)

Signed-off-by: Trung <[email protected]>
  • Loading branch information
ichbinfrog authored Sep 7, 2022
1 parent cbe3d24 commit a1a3ed5
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 26 deletions.
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)
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)
}
})
}
}

0 comments on commit a1a3ed5

Please sign in to comment.