Repository: dexidp/dex
Pull request #3550 (2024-05-29 04:44:36)

build(deps): bump golang from `ff6ab2b` to `d1a601b`
Bumps golang from `ff6ab2b` to `d1a601b`.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang&package-manager=docker&previous-version=1.22.3-alpine3.18&new-version=1.22.3-alpine3.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/artifacts.yaml b/.github/workflows/artifacts.yaml
index e802befe..a3ca03a3 100644
--- a/.github/workflows/artifacts.yaml
+++ b/.github/workflows/artifacts.yaml
@@ -103,7 +103,7 @@ jobs:
           if_false: type=oci,dest=image.tar
 
       - name: Login to GitHub Container Registry
-        uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
+        uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
@@ -111,7 +111,7 @@ jobs:
         if: inputs.publish
 
       - name: Login to Docker Hub
-        uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
+        uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
         with:
           username: ${{ secrets.DOCKER_USERNAME }}
           password: ${{ secrets.DOCKER_PASSWORD }}
diff --git a/Dockerfile b/Dockerfile
index 20fa098c..e5907ff5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@ ARG BASE_IMAGE=alpine
 
 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.4.0@sha256:0cd3f05c72d6c9b038eb135f91376ee1169ef3a330d34e418e65e2a5c2e9c0d4 AS xx
 
-FROM --platform=$BUILDPLATFORM golang:1.22.3-alpine3.18@sha256:ff6ab2bb00e8a8efd548c6d4b3fc7146698f6e5189475edf42ebf15c33162565 AS builder
+FROM --platform=$BUILDPLATFORM golang:1.22.3-alpine3.18@sha256:d1a601b64de09e2fa38c95e55838961811d5ca11062a8f4230a5c434b3ae2a34 AS builder
 
 COPY --from=xx / /
 
diff --git a/connector/google/google.go b/connector/google/google.go
index c2df1100..c3042970 100644
--- a/connector/google/google.go
+++ b/connector/google/google.go
@@ -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"
@@ -98,8 +100,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 {
@@ -362,25 +363,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)
diff --git a/go.mod b/go.mod
index 107a87cb..80349c2a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/dexidp/dex
 go 1.21
 
 require (
+	cloud.google.com/go/compute/metadata v0.3.0
 	entgo.io/ent v0.13.1
 	github.com/AppsFlyer/go-sundheit v0.5.0
 	github.com/Masterminds/semver v1.5.0
@@ -45,7 +46,6 @@ require (
 	ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
 	cloud.google.com/go/auth v0.4.1 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
-	cloud.google.com/go/compute/metadata v0.3.0 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index d10acb0c..54f6f5a4 100644
--- a/go.sum
+++ b/go.sum
@@ -231,6 +231,8 @@ go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js
 go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
 go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
@@ -327,6 +329,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=


Test patch:
diff --git a/connector/google/google_test.go b/connector/google/google_test.go
index c56a5e78..2fa2b783 100644
--- a/connector/google/google_test.go
+++ b/connector/google/google_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/sirupsen/logrus"
@@ -295,6 +296,103 @@ 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("my-service-account@example-project.iam.gserviceaccount.com")
+	})
+	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": "admin@dexidp.com"},
+	})
+	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": "admin@dexidp.com"},
+	})
+	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:     "user_1@dexidp.com",
+			expectedErr: "",
+		},
+		"wrong_user_request": {
+			userKey:     "user_1@foo.bar",
+			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)
+			}
+		})
+	}
+}
+
 func TestPromptTypeConfig(t *testing.T) {
 	promptTypeLogin := "login"
 	cases := []struct {