mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
451 lines
13 KiB
451 lines
13 KiB
package google |
|
|
|
import ( |
|
"context" |
|
"encoding/json" |
|
"fmt" |
|
"log/slog" |
|
"net/http" |
|
"net/http/httptest" |
|
"net/url" |
|
"os" |
|
"strings" |
|
"testing" |
|
|
|
"github.com/stretchr/testify/assert" |
|
admin "google.golang.org/api/admin/directory/v1" |
|
"google.golang.org/api/option" |
|
|
|
"github.com/dexidp/dex/connector" |
|
) |
|
|
|
var ( |
|
// groups_0 |
|
// ┌───────┤ |
|
// groups_2 groups_1 |
|
// │ ├────────┐ |
|
// └── user_1 user_2 |
|
testGroups = map[string][]*admin.Group{ |
|
"user_1@dexidp.com": {{Email: "groups_2@dexidp.com"}, {Email: "groups_1@dexidp.com"}}, |
|
"user_2@dexidp.com": {{Email: "groups_1@dexidp.com"}}, |
|
"groups_1@dexidp.com": {{Email: "groups_0@dexidp.com"}}, |
|
"groups_2@dexidp.com": {{Email: "groups_0@dexidp.com"}}, |
|
"groups_0@dexidp.com": {}, |
|
} |
|
callCounter = make(map[string]int) |
|
) |
|
|
|
func testSetup() *httptest.Server { |
|
mux := http.NewServeMux() |
|
|
|
mux.HandleFunc("/admin/directory/v1/groups/", func(w http.ResponseWriter, r *http.Request) { |
|
w.Header().Add("Content-Type", "application/json") |
|
userKey := r.URL.Query().Get("userKey") |
|
if groups, ok := testGroups[userKey]; ok { |
|
json.NewEncoder(w).Encode(admin.Groups{Groups: groups}) |
|
callCounter[userKey]++ |
|
} |
|
}) |
|
|
|
return httptest.NewServer(mux) |
|
} |
|
|
|
func newConnector(config *Config) (*googleConnector, error) { |
|
log := slog.New(slog.DiscardHandler) |
|
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() |
|
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 upon 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"}, |
|
ServiceAccountFilePath: serviceAccountFilePath, |
|
}, |
|
expectedErr: "requires the domainToAdminEmail", |
|
}, |
|
"service_account_key_not_found": { |
|
config: &Config{ |
|
ClientID: "testClient", |
|
ClientSecret: "testSecret", |
|
RedirectURI: ts.URL + "/callback", |
|
Scopes: []string{"openid", "groups"}, |
|
DomainToAdminEmail: map[string]string{"*": "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"}, |
|
DomainToAdminEmail: map[string]string{"bar.com": "foo@bar.com"}, |
|
ServiceAccountFilePath: serviceAccountFilePath, |
|
}, |
|
expectedErr: "", |
|
}, |
|
"adc": { |
|
config: &Config{ |
|
ClientID: "testClient", |
|
ClientSecret: "testSecret", |
|
RedirectURI: ts.URL + "/callback", |
|
Scopes: []string{"openid", "groups"}, |
|
DomainToAdminEmail: map[string]string{"*": "foo@bar.com"}, |
|
}, |
|
adc: serviceAccountFilePath, |
|
expectedErr: "", |
|
}, |
|
"adc_priority": { |
|
config: &Config{ |
|
ClientID: "testClient", |
|
ClientSecret: "testSecret", |
|
RedirectURI: ts.URL + "/callback", |
|
Scopes: []string{"openid", "groups"}, |
|
DomainToAdminEmail: map[string]string{"*": "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) |
|
|
|
if reference.expectedErr == "" { |
|
assert.Nil(err) |
|
assert.NotNil(conn) |
|
} else { |
|
assert.ErrorContains(err, reference.expectedErr) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
func TestGetGroups(t *testing.T) { |
|
ts := testSetup() |
|
defer ts.Close() |
|
|
|
serviceAccountFilePath, err := tempServiceAccountKey() |
|
assert.Nil(t, err) |
|
|
|
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFilePath) |
|
conn, err := newConnector(&Config{ |
|
ClientID: "testClient", |
|
ClientSecret: "testSecret", |
|
RedirectURI: ts.URL + "/callback", |
|
Scopes: []string{"openid", "groups"}, |
|
DomainToAdminEmail: map[string]string{"*": "admin@dexidp.com"}, |
|
}) |
|
assert.Nil(t, err) |
|
|
|
conn.adminSrv[wildcardDomainToAdminEmail], err = admin.NewService(context.Background(), option.WithoutAuthentication(), option.WithEndpoint(ts.URL)) |
|
assert.Nil(t, err) |
|
type testCase struct { |
|
userKey string |
|
fetchTransitiveGroupMembership bool |
|
shouldErr bool |
|
expectedGroups []string |
|
} |
|
|
|
for name, testCase := range map[string]testCase{ |
|
"user1_non_transitive_lookup": { |
|
userKey: "user_1@dexidp.com", |
|
fetchTransitiveGroupMembership: false, |
|
shouldErr: false, |
|
expectedGroups: []string{"groups_1@dexidp.com", "groups_2@dexidp.com"}, |
|
}, |
|
"user1_transitive_lookup": { |
|
userKey: "user_1@dexidp.com", |
|
fetchTransitiveGroupMembership: true, |
|
shouldErr: false, |
|
expectedGroups: []string{"groups_0@dexidp.com", "groups_1@dexidp.com", "groups_2@dexidp.com"}, |
|
}, |
|
"user2_non_transitive_lookup": { |
|
userKey: "user_2@dexidp.com", |
|
fetchTransitiveGroupMembership: false, |
|
shouldErr: false, |
|
expectedGroups: []string{"groups_1@dexidp.com"}, |
|
}, |
|
"user2_transitive_lookup": { |
|
userKey: "user_2@dexidp.com", |
|
fetchTransitiveGroupMembership: true, |
|
shouldErr: false, |
|
expectedGroups: []string{"groups_0@dexidp.com", "groups_1@dexidp.com"}, |
|
}, |
|
} { |
|
testCase := testCase |
|
callCounter = map[string]int{} |
|
t.Run(name, func(t *testing.T) { |
|
assert := assert.New(t) |
|
lookup := make(map[string]struct{}) |
|
|
|
groups, err := conn.getGroups(testCase.userKey, testCase.fetchTransitiveGroupMembership, lookup) |
|
if testCase.shouldErr { |
|
assert.NotNil(err) |
|
} else { |
|
assert.Nil(err) |
|
} |
|
assert.ElementsMatch(testCase.expectedGroups, groups) |
|
t.Logf("[%s] Amount of API calls per userKey: %+v\n", t.Name(), callCounter) |
|
}) |
|
} |
|
} |
|
|
|
func TestDomainToAdminEmailConfig(t *testing.T) { |
|
ts := testSetup() |
|
defer ts.Close() |
|
|
|
serviceAccountFilePath, err := tempServiceAccountKey() |
|
assert.Nil(t, err) |
|
|
|
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFilePath) |
|
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", |
|
}, |
|
} { |
|
testCase := testCase |
|
callCounter = map[string]int{} |
|
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) |
|
} |
|
t.Logf("[%s] Amount of API calls per userKey: %+v\n", t.Name(), callCounter) |
|
}) |
|
} |
|
} |
|
|
|
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 { |
|
name string |
|
promptType *string |
|
expectedPromptTypeValue string |
|
}{ |
|
{ |
|
name: "prompt type is nil", |
|
promptType: nil, |
|
expectedPromptTypeValue: "consent", |
|
}, |
|
{ |
|
name: "prompt type is empty", |
|
promptType: new(string), |
|
expectedPromptTypeValue: "", |
|
}, |
|
{ |
|
name: "prompt type is set", |
|
promptType: &promptTypeLogin, |
|
expectedPromptTypeValue: "login", |
|
}, |
|
} |
|
|
|
ts := testSetup() |
|
defer ts.Close() |
|
|
|
serviceAccountFilePath, err := tempServiceAccountKey() |
|
assert.Nil(t, err) |
|
|
|
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFilePath) |
|
|
|
for _, test := range cases { |
|
t.Run(test.name, func(t *testing.T) { |
|
conn, err := newConnector(&Config{ |
|
ClientID: "testClient", |
|
ClientSecret: "testSecret", |
|
RedirectURI: ts.URL + "/callback", |
|
Scopes: []string{"openid", "groups", "offline_access"}, |
|
DomainToAdminEmail: map[string]string{"dexidp.com": "admin@dexidp.com"}, |
|
PromptType: test.promptType, |
|
}) |
|
|
|
assert.Nil(t, err) |
|
assert.Equal(t, test.expectedPromptTypeValue, conn.promptType) |
|
|
|
loginURL, err := conn.LoginURL(connector.Scopes{OfflineAccess: true}, ts.URL+"/callback", "state") |
|
assert.Nil(t, err) |
|
|
|
urlp, err := url.Parse(loginURL) |
|
assert.Nil(t, err) |
|
|
|
assert.Equal(t, test.expectedPromptTypeValue, urlp.Query().Get("prompt")) |
|
}) |
|
} |
|
}
|
|
|