Skip to content

Commit

Permalink
implement groups fetch by default service account from metadata
Browse files Browse the repository at this point in the history
Signed-off-by: Viacheslav Sychov <[email protected]>
  • Loading branch information
vsychov committed Jun 8, 2023
1 parent 2205bd9 commit eba0f71
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 10 deletions.
77 changes: 68 additions & 9 deletions connector/google/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (
"strings"
"time"

"cloud.google.com/go/compute/metadata"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/exp/slices"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/impersonate"
"google.golang.org/api/option"

"github.com/dexidp/dex/connector"
Expand Down Expand Up @@ -94,8 +96,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
return nil, fmt.Errorf("directory service requires the domainToAdminEmail option to be configured")
}

// Fixing a regression caused by default config fallback: https://github.com/dexidp/dex/issues/2699
if (c.ServiceAccountFilePath != "" && len(c.DomainToAdminEmail) > 0) || slices.Contains(scopes, "groups") {
if (len(c.DomainToAdminEmail) > 0) || slices.Contains(scopes, "groups") {
for domain, adminEmail := range c.DomainToAdminEmail {
srv, err := createDirectoryService(c.ServiceAccountFilePath, adminEmail, logger)
if err != nil {
Expand Down Expand Up @@ -350,25 +351,83 @@ func (c *googleConnector) extractDomainFromEmail(email string) string {
return wildcardDomainToAdminEmail
}

// getCredentialsFromFilePath reads and returns the service account credentials from the file at the provided path.
// If an error occurs during the read, it is returned.
func getCredentialsFromFilePath(serviceAccountFilePath string) ([]byte, error) {
jsonCredentials, err := os.ReadFile(serviceAccountFilePath)
if err != nil {
return nil, fmt.Errorf("error reading credentials from file: %v", err)
}
return jsonCredentials, nil
}

// getCredentialsFromDefault retrieves the application's default credentials.
// If the default credential is empty, it attempts to create a new service with metadata credentials.
// If successful, it returns the service and nil error.
// If unsuccessful, it returns the error and a nil service.
func getCredentialsFromDefault(ctx context.Context, email string, logger log.Logger) ([]byte, *admin.Service, error) {
credential, err := google.FindDefaultCredentials(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to fetch application default credentials: %w", err)
}

if credential.JSON == nil {
logger.Info("JSON is empty, using flow for GCE")
service, err := createServiceWithMetadataServer(ctx, email, logger)
if err != nil {
return nil, nil, err
}
return nil, service, nil
}

return credential.JSON, nil, nil
}

// createServiceWithMetadataServer creates a new service using metadata server.
// If an error occurs during the process, it is returned along with a nil service.
func createServiceWithMetadataServer(ctx context.Context, adminEmail string, logger log.Logger) (*admin.Service, error) {
serviceAccountEmail, err := metadata.Email("default")
logger.Infof("discovered serviceAccountEmail: %s", serviceAccountEmail)

if err != nil {
return nil, fmt.Errorf("unable to get service account email from metadata server: %v", err)
}

config := impersonate.CredentialsConfig{
TargetPrincipal: serviceAccountEmail,
Scopes: []string{admin.AdminDirectoryGroupReadonlyScope},
Lifetime: 0,
Subject: adminEmail,
}

tokenSource, err := impersonate.CredentialsTokenSource(ctx, config)
if err != nil {
return nil, fmt.Errorf("unable to impersonate with %s, error: %v", adminEmail, err)
}

return admin.NewService(ctx, option.WithHTTPClient(oauth2.NewClient(ctx, tokenSource)))
}

// 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) {
func createDirectoryService(serviceAccountFilePath, email string, logger log.Logger) (service *admin.Service, err error) {
var jsonCredentials []byte
var err error

ctx := context.Background()
if serviceAccountFilePath == "" {
logger.Warn("the application default credential is used since the service account file path is not used")
credential, err := google.FindDefaultCredentials(ctx)
jsonCredentials, service, err = getCredentialsFromDefault(ctx, email, logger)
if err != nil {
return nil, fmt.Errorf("failed to fetch application default credentials: %w", err)
return
}
if service != nil {
return
}
jsonCredentials = credential.JSON
} else {
jsonCredentials, err = os.ReadFile(serviceAccountFilePath)
jsonCredentials, err = getCredentialsFromFilePath(serviceAccountFilePath)
if err != nil {
return nil, fmt.Errorf("error reading credentials from file: %v", err)
return
}
}
config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope)
Expand Down
98 changes: 98 additions & 0 deletions connector/google/google_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -291,3 +292,100 @@ func TestDomainToAdminEmailConfig(t *testing.T) {
})
}
}

var gceMetadataFlags = map[string]bool{
"failOnEmailRequest": false,
}

func mockGCEMetadataServer() *httptest.Server {
mux := http.NewServeMux()

mux.HandleFunc("/computeMetadata/v1/instance/service-accounts/default/email", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
if gceMetadataFlags["failOnEmailRequest"] {
w.WriteHeader(http.StatusBadRequest)
}
json.NewEncoder(w).Encode("[email protected]")
})
mux.HandleFunc("/computeMetadata/v1/instance/service-accounts/default/token", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(struct {
AccessToken string `json:"access_token"`
ExpiresInSec int `json:"expires_in"`
TokenType string `json:"token_type"`
}{
AccessToken: "my-example.token",
ExpiresInSec: 3600,
TokenType: "Bearer",
})
})

return httptest.NewServer(mux)
}

func TestGCEWorkloadIdentity(t *testing.T) {
ts := testSetup()
defer ts.Close()

metadataServer := mockGCEMetadataServer()
defer metadataServer.Close()
metadataServerHost := strings.Replace(metadataServer.URL, "http://", "", 1)

os.Setenv("GCE_METADATA_HOST", metadataServerHost)
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "")
os.Setenv("HOME", "/tmp")

gceMetadataFlags["failOnEmailRequest"] = true
_, err := newConnector(&Config{
ClientID: "testClient",
ClientSecret: "testSecret",
RedirectURI: ts.URL + "/callback",
Scopes: []string{"openid", "groups"},
DomainToAdminEmail: map[string]string{"dexidp.com": "[email protected]"},
})
assert.Error(t, err)

gceMetadataFlags["failOnEmailRequest"] = false
conn, err := newConnector(&Config{
ClientID: "testClient",
ClientSecret: "testSecret",
RedirectURI: ts.URL + "/callback",
Scopes: []string{"openid", "groups"},
DomainToAdminEmail: map[string]string{"dexidp.com": "[email protected]"},
})
assert.Nil(t, err)

conn.adminSrv["dexidp.com"], err = admin.NewService(context.Background(), option.WithoutAuthentication(), option.WithEndpoint(ts.URL))
assert.Nil(t, err)
type testCase struct {
userKey string
expectedErr string
}

for name, testCase := range map[string]testCase{
"correct_user_request": {
userKey: "[email protected]",
expectedErr: "",
},
"wrong_user_request": {
userKey: "[email protected]",
expectedErr: "unable to find super admin email",
},
"wrong_connector_response": {
userKey: "user_1_foo.bar",
expectedErr: "unable to find super admin email",
},
} {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
lookup := make(map[string]struct{})

_, err := conn.getGroups(testCase.userKey, true, lookup)
if testCase.expectedErr != "" {
assert.ErrorContains(err, testCase.expectedErr)
} else {
assert.Nil(err)
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/dexidp/dex
go 1.20

require (
cloud.google.com/go/compute/metadata v0.2.3
entgo.io/ent v0.12.3
github.com/AppsFlyer/go-sundheit v0.5.0
github.com/Masterminds/semver v1.5.0
Expand Down Expand Up @@ -43,7 +44,6 @@ require (
require (
ariga.io/atlas v0.10.2-0.20230427182402-87a07dfb83bf // indirect
cloud.google.com/go/compute v1.19.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
Expand Down

0 comments on commit eba0f71

Please sign in to comment.