Skip to content
Snippets Groups Projects
Unverified Commit a1a3ed5b authored by Hoang Quoc Trung's avatar Hoang Quoc Trung Committed by GitHub
Browse files

Implement Application Default Credentials for the google connector (#2530)

parent cbe3d245
No related branches found
No related tags found
No related merge requests found
...@@ -71,7 +71,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e ...@@ -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 ...@@ -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 {
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 { 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 // uniqueGroups returns the unique groups of a slice
......
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)
}
})
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment