From 44499bc8b0a39fe30f1bf1181964a97c1e90fa04 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Tue, 10 Apr 2018 09:47:59 -0400 Subject: [PATCH 01/30] Add new connector for Cloudfoundry - Verifies user is part of orgs and spaces for group claims Signed-off-by: Joshua Winters Co-authored-by: Shash Reddy --- connector/cf/cf.go | 303 ++++++++++++++++++++++++++++++++++++++++ connector/cf/cf_test.go | 191 +++++++++++++++++++++++++ server/server.go | 2 + 3 files changed, 496 insertions(+) create mode 100644 connector/cf/cf.go create mode 100644 connector/cf/cf_test.go diff --git a/connector/cf/cf.go b/connector/cf/cf.go new file mode 100644 index 0000000000..2e4d2243e0 --- /dev/null +++ b/connector/cf/cf.go @@ -0,0 +1,303 @@ +package cf + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "strings" + "time" + + "github.com/dexidp/dex/connector" + "github.com/dexidp/dex/pkg/log" + "golang.org/x/oauth2" +) + +type cfConnector struct { + clientID string + clientSecret string + redirectURI string + apiURL string + tokenURL string + authorizationURL string + userInfoURL string + httpClient *http.Client + logger log.Logger +} + +type connectorData struct { + AccessToken string +} + +type Config struct { + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + APIURL string `json:"apiURL"` + RootCAs []string `json:"rootCAs"` + InsecureSkipVerify bool `json:"insecureSkipVerify"` +} + +type CCResponse struct { + Resources []Resource `json:"resources"` + TotalResults int `json:"total_results"` +} + +type Resource struct { + Metadata Metadata `json:"metadata"` + Entity Entity `json:"entity"` +} + +type Metadata struct { + Guid string `json:"guid"` +} + +type Entity struct { + Name string `json:"name"` + OrganizationGuid string `json:"organization_guid"` +} + +func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { + var err error + + cfConn := &cfConnector{ + clientID: c.ClientID, + clientSecret: c.ClientSecret, + apiURL: c.APIURL, + redirectURI: c.RedirectURI, + logger: logger, + } + + cfConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) + if err != nil { + return nil, err + } + + apiURL := strings.TrimRight(c.APIURL, "/") + apiResp, err := cfConn.httpClient.Get(fmt.Sprintf("%s/v2/info", apiURL)) + + if err != nil { + logger.Errorf("failed-to-send-request-to-cloud-controller-api", err) + return nil, err + } + + defer apiResp.Body.Close() + + if apiResp.StatusCode != http.StatusOK { + err = errors.New(fmt.Sprintf("request failed with status %d", apiResp.StatusCode)) + logger.Errorf("failed-get-info-response-from-api", err) + return nil, err + } + + var apiResult map[string]interface{} + json.NewDecoder(apiResp.Body).Decode(&apiResult) + + uaaURL := strings.TrimRight(apiResult["token_endpoint"].(string), "/") + uaaResp, err := cfConn.httpClient.Get(fmt.Sprintf("%s/.well-known/openid-configuration", uaaURL)) + + if err != nil { + logger.Errorf("failed-to-send-request-to-uaa-api", err) + return nil, err + } + + if apiResp.StatusCode != http.StatusOK { + err = errors.New(fmt.Sprintf("request failed with status %d", apiResp.StatusCode)) + logger.Errorf("failed-to-get-well-known-config-repsonse-from-api", err) + return nil, err + } + + defer uaaResp.Body.Close() + + var uaaResult map[string]interface{} + err = json.NewDecoder(uaaResp.Body).Decode(&uaaResult) + + if err != nil { + logger.Errorf("failed-to-decode-response-from-uaa-api", err) + return nil, err + } + + cfConn.tokenURL, _ = uaaResult["token_endpoint"].(string) + cfConn.authorizationURL, _ = uaaResult["authorization_endpoint"].(string) + cfConn.userInfoURL, _ = uaaResult["userinfo_endpoint"].(string) + + return cfConn, err +} + +func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) { + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} + for _, rootCA := range rootCAs { + rootCABytes, err := ioutil.ReadFile(rootCA) + if err != nil { + return nil, fmt.Errorf("failed to read root-ca: %v", err) + } + if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { + return nil, fmt.Errorf("no certs found in root CA file %q", rootCA) + } + } + + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, nil +} + +func (c *cfConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { + + if c.redirectURI != callbackURL { + return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + } + + oauth2Config := &oauth2.Config{ + ClientID: c.clientID, + ClientSecret: c.clientSecret, + Endpoint: oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL}, + RedirectURL: c.redirectURI, + Scopes: []string{"openid", "cloud_controller.read"}, + } + + return oauth2Config.AuthCodeURL(state), nil +} + +func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { + + q := r.URL.Query() + if errType := q.Get("error"); errType != "" { + return identity, errors.New(q.Get("error_description")) + } + + oauth2Config := &oauth2.Config{ + ClientID: c.clientID, + ClientSecret: c.clientSecret, + Endpoint: oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL}, + RedirectURL: c.redirectURI, + Scopes: []string{"openid", "cloud_controller.read"}, + } + + ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient) + + token, err := oauth2Config.Exchange(ctx, q.Get("code")) + if err != nil { + return identity, fmt.Errorf("CF connector: failed to get token: %v", err) + } + + client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)) + + userInfoResp, err := client.Get(c.userInfoURL) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to execute request to userinfo: %v", err) + } + + if userInfoResp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("CF Connector: failed to execute request to userinfo: status %d", userInfoResp.StatusCode) + } + + defer userInfoResp.Body.Close() + + var userInfoResult map[string]interface{} + err = json.NewDecoder(userInfoResp.Body).Decode(&userInfoResult) + + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to parse userinfo: %v", err) + } + + identity.UserID, _ = userInfoResult["user_id"].(string) + identity.Username, _ = userInfoResult["user_name"].(string) + identity.PreferredUsername, _ = userInfoResult["user_name"].(string) + identity.Email, _ = userInfoResult["email"].(string) + identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) + + if s.Groups { + // fetch orgs + orgsResp, err := client.Get(fmt.Sprintf("%s/v2/users/%s/organizations", c.apiURL, identity.UserID)) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: %v", err) + } + if orgsResp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: status %d", orgsResp.StatusCode) + } + + var orgs CCResponse + + err = json.NewDecoder(orgsResp.Body).Decode(&orgs) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to parse orgs: %v", err) + } + + var orgMap = make(map[string]string) + var orgSpaces = make(map[string][]string) + + for _, resource := range orgs.Resources { + orgMap[resource.Metadata.Guid] = resource.Entity.Name + orgSpaces[resource.Entity.Name] = []string{} + } + + // fetch spaces + spacesResp, err := client.Get(fmt.Sprintf("%s/v2/users/%s/spaces", c.apiURL, identity.UserID)) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: %v", err) + } + if spacesResp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: status %d", spacesResp.StatusCode) + } + + var spaces CCResponse + + err = json.NewDecoder(spacesResp.Body).Decode(&spaces) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to parse spaces: %v", err) + } + + var groupsClaims []string + + for _, resource := range spaces.Resources { + orgName := orgMap[resource.Entity.OrganizationGuid] + orgSpaces[orgName] = append(orgSpaces[orgName], resource.Entity.Name) + + groupsClaims = append(groupsClaims, resource.Metadata.Guid) + } + + for orgName, spaceNames := range orgSpaces { + if len(spaceNames) > 0 { + for _, spaceName := range spaceNames { + groupsClaims = append(groupsClaims, fmt.Sprintf("%s:%s", orgName, spaceName)) + } + } else { + groupsClaims = append(groupsClaims, fmt.Sprintf("%s", orgName)) + } + } + + identity.Groups = groupsClaims + } + + if s.OfflineAccess { + data := connectorData{AccessToken: token.AccessToken} + connData, err := json.Marshal(data) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to parse connector data for offline access: %v", err) + } + identity.ConnectorData = connData + } + + return identity, nil +} diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go new file mode 100644 index 0000000000..6680da0b16 --- /dev/null +++ b/connector/cf/cf_test.go @@ -0,0 +1,191 @@ +package cf + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "sort" + "strings" + "testing" + + "github.com/dexidp/dex/connector" + "github.com/sirupsen/logrus" +) + +func TestOpen(t *testing.T) { + testServer := testSetup() + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + + expectEqual(t, conn.clientID, "test-client") + expectEqual(t, conn.clientSecret, "secret") + expectEqual(t, conn.redirectURI, testServer.URL+"/callback") +} + +func TestHandleCallback(t *testing.T) { + + testServer := testSetup() + defer testServer.Close() + + cfConn := &cfConnector{ + tokenURL: fmt.Sprintf("%s/token", testServer.URL), + authorizationURL: fmt.Sprintf("%s/authorize", testServer.URL), + userInfoURL: fmt.Sprintf("%s/userinfo", testServer.URL), + apiURL: testServer.URL, + clientSecret: "secret", + clientID: "test-client", + redirectURI: "localhost:8080/sky/dex/callback", + httpClient: http.DefaultClient, + } + + req, err := http.NewRequest("GET", testServer.URL, nil) + expectEqual(t, err, nil) + + t.Run("CallbackWithGroupsScope", func(t *testing.T) { + identity, err := cfConn.HandleCallback(connector.Scopes{Groups: true}, req) + expectEqual(t, err, nil) + + sort.Strings(identity.Groups) + expectEqual(t, len(identity.Groups), 3) + expectEqual(t, identity.Groups[0], "some-org-name-1:some-space-name") + expectEqual(t, identity.Groups[1], "some-org-name-2") + expectEqual(t, identity.Groups[2], "some-space-guid") + }) + + t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { + identity, err := cfConn.HandleCallback(connector.Scopes{}, req) + + expectEqual(t, err, nil) + expectEqual(t, identity.UserID, "12345") + expectEqual(t, identity.Username, "test-user") + }) + + t.Run("CallbackWithOfflineAccessScope", func(t *testing.T) { + identity, err := cfConn.HandleCallback(connector.Scopes{OfflineAccess: true}, req) + + expectEqual(t, err, nil) + expectNotEqual(t, len(identity.ConnectorData), 0) + + cData := connectorData{} + err = json.Unmarshal(identity.ConnectorData, &cData) + + expectEqual(t, err, nil) + expectNotEqual(t, cData.AccessToken, "") + }) +} + +func testSetup() *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { + token := "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIiwidHlwIjoiSldUIn0.eyJqdGkiOiIxMjk4MTNhZjJiNGM0ZDNhYmYyNjljMzM4OTFkZjNiZCIsInN1YiI6ImNmMWFlODk4LWQ1ODctNDBhYS1hNWRiLTE5ZTY3MjI0N2I1NyIsInNjb3BlIjpbImNsb3VkX2NvbnRyb2xsZXIucmVhZCIsIm9wZW5pZCJdLCJjbGllbnRfaWQiOiJjb25jb3Vyc2UiLCJjaWQiOiJjb25jb3Vyc2UiLCJhenAiOiJjb25jb3Vyc2UiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6ImNmMWFlODk4LWQ1ODctNDBhYS1hNWRiLTE5ZTY3MjI0N2I1NyIsIm9yaWdpbiI6InVhYSIsInVzZXJfbmFtZSI6ImFkbWluIiwiZW1haWwiOiJhZG1pbiIsImF1dGhfdGltZSI6MTUyMzM3NDIwNCwicmV2X3NpZyI6IjYxNWJjMTk0IiwiaWF0IjoxNTIzMzc3MTUyLCJleHAiOjE1MjM0MjAzNTIsImlzcyI6Imh0dHBzOi8vdWFhLnN0eXgucHVzaC5nY3AuY2YtYXBwLmNvbS9vYXV0aC90b2tlbiIsInppZCI6InVhYSIsImF1ZCI6WyJjbG91ZF9jb250cm9sbGVyIiwiY29uY291cnNlIiwib3BlbmlkIl19.FslbnwvW0WScVRNK8IWghRX0buXfl6qaI1K7z_dzjPUVrdEyMtaYa3kJI8srA-2G1PjSSEWa_3Vzs_BEnTc3iG0JQWU0XlcjdCdAFTvnmKiHSzffy1O_oGYyH47KXtnZOxHf3rdV_Xgw4XTqPrfKXQxnPemUAJyKf2tjgs3XToGaqqBw-D_2BQVY79kF0_GgksQsViqq1GW0Dur6m2CgBhtc2h1AQGO16izXl3uNbpW6ClhaW43NQXlE4wqtr7kfmxyOigHJb2MSQ3wwPc6pqYdUT6ka_TMqavqbxEJ4QcS6SoEcVsDTmEQ4c8dmWUgXM0AZjd0CaEGTB6FDHxH5sw" + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "access_token": token, + }) + }) + + mux.HandleFunc("/v2/info", func(w http.ResponseWriter, r *http.Request) { + url := fmt.Sprintf("http://%s", r.Host) + + json.NewEncoder(w).Encode(map[string]string{ + "token_endpoint": url, + }) + }) + + mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { + url := fmt.Sprintf("http://%s", r.Host) + + json.NewEncoder(w).Encode(map[string]string{ + "token_endpoint": url, + "authorization_endpoint": url, + "userinfo_endpoint": url, + }) + }) + + mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + }) + + mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]string{ + "user_id": "12345", + "user_name": "test-user", + "email": "blah-email", + }) + }) + + mux.HandleFunc("/v2/users/", func(w http.ResponseWriter, r *http.Request) { + var result map[string]interface{} + + if strings.Contains(r.URL.String(), "spaces") { + result = map[string]interface{}{ + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-space-guid"}, + "entity": map[string]string{"name": "some-space-name", "organization_guid": "some-org-guid-1"}, + }, + }, + } + } + + if strings.Contains(r.URL.String(), "organizations") { + result = map[string]interface{}{ + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-org-guid-1"}, + "entity": map[string]string{"name": "some-org-name-1"}, + }, + { + "metadata": map[string]string{"guid": "some-org-guid-2"}, + "entity": map[string]string{"name": "some-org-name-2"}, + }, + }, + } + } + json.NewEncoder(w).Encode(result) + }) + + return httptest.NewServer(mux) +} + +func newConnector(t *testing.T, serverURL string) *cfConnector { + + callBackURL := fmt.Sprintf("%s/callback", serverURL) + + testConfig := Config{ + APIURL: serverURL, + ClientID: "test-client", + ClientSecret: "secret", + RedirectURI: callBackURL, + InsecureSkipVerify: true, + } + + log := logrus.New() + + conn, err := testConfig.Open("id", log) + if err != nil { + t.Fatal(err) + } + + cfConn, ok := conn.(*cfConnector) + if !ok { + t.Fatal(errors.New("it is not a cf conn")) + } + + return cfConn +} + +func expectEqual(t *testing.T, a interface{}, b interface{}) { + if !reflect.DeepEqual(a, b) { + t.Fatalf("Expected %+v to equal %+v", a, b) + } +} + +func expectNotEqual(t *testing.T, a interface{}, b interface{}) { + if reflect.DeepEqual(a, b) { + t.Fatalf("Expected %+v to NOT equal %+v", a, b) + } +} diff --git a/server/server.go b/server/server.go index a79b7cfd3b..38aad89a03 100644 --- a/server/server.go +++ b/server/server.go @@ -26,6 +26,7 @@ import ( "github.com/dexidp/dex/connector/atlassiancrowd" "github.com/dexidp/dex/connector/authproxy" "github.com/dexidp/dex/connector/bitbucketcloud" + "github.com/dexidp/dex/connector/cf" "github.com/dexidp/dex/connector/gitea" "github.com/dexidp/dex/connector/github" "github.com/dexidp/dex/connector/gitlab" @@ -508,6 +509,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{ "bitbucket-cloud": func() ConnectorConfig { return new(bitbucketcloud.Config) }, "openshift": func() ConnectorConfig { return new(openshift.Config) }, "atlassian-crowd": func() ConnectorConfig { return new(atlassiancrowd.Config) }, + "cf": func() ConnectorConfig { return new(cf.Config) }, // Keep around for backwards compatibility. "samlExperimental": func() ConnectorConfig { return new(saml.Config) }, } From bd45301238eb6dca7591963bb6d292ee226a8be7 Mon Sep 17 00:00:00 2001 From: Josh Winters Date: Thu, 4 Oct 2018 15:07:26 -0400 Subject: [PATCH 02/30] update cf connector to use 'authorization_endpoint' from /v2/info Co-authored-by: Topher Bullock Signed-off-by: Josh Winters --- connector/cf/cf.go | 2 +- connector/cf/cf_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 2e4d2243e0..4452e2f99e 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -97,7 +97,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) var apiResult map[string]interface{} json.NewDecoder(apiResp.Body).Decode(&apiResult) - uaaURL := strings.TrimRight(apiResult["token_endpoint"].(string), "/") + uaaURL := strings.TrimRight(apiResult["authorization_endpoint"].(string), "/") uaaResp, err := cfConn.httpClient.Get(fmt.Sprintf("%s/.well-known/openid-configuration", uaaURL)) if err != nil { diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index 6680da0b16..bd1026bd6a 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -92,7 +92,7 @@ func testSetup() *httptest.Server { url := fmt.Sprintf("http://%s", r.Host) json.NewEncoder(w).Encode(map[string]string{ - "token_endpoint": url, + "authorization_endpoint": url, }) }) From b5879aa67f0a06f5a7429c082b45a76c5624efd7 Mon Sep 17 00:00:00 2001 From: Daniel Lavoie Date: Thu, 4 Apr 2019 18:26:59 -0400 Subject: [PATCH 03/30] Added support for CF resources pagination Signed-off-by: Daniel Lavoie --- connector/cf/cf.go | 81 ++++++++++++++++++++++++----------------- connector/cf/cf_test.go | 70 +++++++++++++++++++++++++---------- 2 files changed, 99 insertions(+), 52 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 4452e2f99e..db185307d7 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -44,6 +44,7 @@ type Config struct { } type CCResponse struct { + NextUrl string `json:"next_url"` Resources []Resource `json:"resources"` TotalResults int `json:"total_results"` } @@ -227,54 +228,68 @@ func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (ident identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) + var orgMap = make(map[string]string) + var orgSpaces = make(map[string][]string) + var groupsClaims []string + if s.Groups { // fetch orgs - orgsResp, err := client.Get(fmt.Sprintf("%s/v2/users/%s/organizations", c.apiURL, identity.UserID)) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: %v", err) - } - if orgsResp.StatusCode != http.StatusOK { - return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: status %d", orgsResp.StatusCode) - } var orgs CCResponse + var nextUrl = fmt.Sprintf("%s/v2/users/%s/organizations", c.apiURL, identity.UserID) + for moreResults := true; moreResults; moreResults = orgs.NextUrl != "" { + orgsResp, err := client.Get(nextUrl) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: %v", err) + } + if orgsResp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: status %d", orgsResp.StatusCode) + } - err = json.NewDecoder(orgsResp.Body).Decode(&orgs) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to parse orgs: %v", err) - } + orgs = CCResponse{} + err = json.NewDecoder(orgsResp.Body).Decode(&orgs) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to parse orgs: %v", err) + } - var orgMap = make(map[string]string) - var orgSpaces = make(map[string][]string) + for _, resource := range orgs.Resources { + orgMap[resource.Metadata.Guid] = resource.Entity.Name + orgSpaces[resource.Entity.Name] = []string{} + } - for _, resource := range orgs.Resources { - orgMap[resource.Metadata.Guid] = resource.Entity.Name - orgSpaces[resource.Entity.Name] = []string{} + if orgs.NextUrl != "" { + nextUrl = fmt.Sprintf("%s%s", c.apiURL, orgs.NextUrl) + } } // fetch spaces - spacesResp, err := client.Get(fmt.Sprintf("%s/v2/users/%s/spaces", c.apiURL, identity.UserID)) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: %v", err) - } - if spacesResp.StatusCode != http.StatusOK { - return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: status %d", spacesResp.StatusCode) - } - var spaces CCResponse + nextUrl = fmt.Sprintf("%s/v2/users/%s/spaces", c.apiURL, identity.UserID) + for moreResults := true; moreResults; moreResults = spaces.NextUrl != "" { + spacesResp, err := client.Get(nextUrl) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: %v", err) + } + if spacesResp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: status %d", spacesResp.StatusCode) + } - err = json.NewDecoder(spacesResp.Body).Decode(&spaces) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to parse spaces: %v", err) - } + spaces = CCResponse{} + err = json.NewDecoder(spacesResp.Body).Decode(&spaces) + if err != nil { + return identity, fmt.Errorf("CF Connector: failed to parse spaces: %v", err) + } - var groupsClaims []string + for _, resource := range spaces.Resources { + orgName := orgMap[resource.Entity.OrganizationGuid] + orgSpaces[orgName] = append(orgSpaces[orgName], resource.Entity.Name) - for _, resource := range spaces.Resources { - orgName := orgMap[resource.Entity.OrganizationGuid] - orgSpaces[orgName] = append(orgSpaces[orgName], resource.Entity.Name) + groupsClaims = append(groupsClaims, resource.Metadata.Guid) + } - groupsClaims = append(groupsClaims, resource.Metadata.Guid) + if spaces.NextUrl != "" { + nextUrl = fmt.Sprintf("%s%s", c.apiURL, spaces.NextUrl) + } } for orgName, spaceNames := range orgSpaces { diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index bd1026bd6a..138dff22db 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -50,10 +50,13 @@ func TestHandleCallback(t *testing.T) { expectEqual(t, err, nil) sort.Strings(identity.Groups) - expectEqual(t, len(identity.Groups), 3) - expectEqual(t, identity.Groups[0], "some-org-name-1:some-space-name") - expectEqual(t, identity.Groups[1], "some-org-name-2") - expectEqual(t, identity.Groups[2], "some-space-guid") + expectEqual(t, len(identity.Groups), 6) + expectEqual(t, identity.Groups[0], "some-org-name-1:some-space-name-1") + expectEqual(t, identity.Groups[1], "some-org-name-2:some-space-name-2") + expectEqual(t, identity.Groups[2], "some-org-name-3") + expectEqual(t, identity.Groups[3], "some-org-name-4") + expectEqual(t, identity.Groups[4], "some-space-guid-1") + expectEqual(t, identity.Groups[5], "some-space-guid-2") }) t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { @@ -121,30 +124,59 @@ func testSetup() *httptest.Server { var result map[string]interface{} if strings.Contains(r.URL.String(), "spaces") { - result = map[string]interface{}{ - "resources": []map[string]interface{}{ - { - "metadata": map[string]string{"guid": "some-space-guid"}, - "entity": map[string]string{"name": "some-space-name", "organization_guid": "some-org-guid-1"}, + if strings.Contains(r.URL.String(), "spaces?order-direction=asc&page=2&results-per-page=50") { + result = map[string]interface{}{ + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-space-guid-2"}, + "entity": map[string]string{"name": "some-space-name-2", "organization_guid": "some-org-guid-2"}, + }, }, - }, + } + } else { + result = map[string]interface{}{ + "next_url": "/v2/users/12345/spaces?order-direction=asc&page=2&results-per-page=50", + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-space-guid-1"}, + "entity": map[string]string{"name": "some-space-name-1", "organization_guid": "some-org-guid-1"}, + }, + }, + } } } if strings.Contains(r.URL.String(), "organizations") { - result = map[string]interface{}{ - "resources": []map[string]interface{}{ - { - "metadata": map[string]string{"guid": "some-org-guid-1"}, - "entity": map[string]string{"name": "some-org-name-1"}, + if strings.Contains(r.URL.String(), "organizations?order-direction=asc&page=2&results-per-page=50") { + result = map[string]interface{}{ + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-org-guid-3"}, + "entity": map[string]string{"name": "some-org-name-3"}, + }, + { + "metadata": map[string]string{"guid": "some-org-guid-4"}, + "entity": map[string]string{"name": "some-org-name-4"}, + }, }, - { - "metadata": map[string]string{"guid": "some-org-guid-2"}, - "entity": map[string]string{"name": "some-org-name-2"}, + } + } else { + result = map[string]interface{}{ + "next_url": "/v2/users/12345/organizations?order-direction=asc&page=2&results-per-page=50", + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-org-guid-1"}, + "entity": map[string]string{"name": "some-org-name-1"}, + }, + { + "metadata": map[string]string{"guid": "some-org-guid-2"}, + "entity": map[string]string{"name": "some-org-name-2"}, + }, }, - }, + } } } + json.NewEncoder(w).Encode(result) }) From b6dd148382a47362f5c56ff1e69ec2d96e58c127 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Thu, 7 Nov 2019 12:36:10 -0500 Subject: [PATCH 04/30] cf: add org to groups claims Signed-off-by: Joshua Winters Co-authored-by: Rui Yang --- connector/cf/cf.go | 9 +++------ connector/cf/cf_test.go | 16 +++++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index db185307d7..0dd76fb821 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -293,12 +293,9 @@ func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (ident } for orgName, spaceNames := range orgSpaces { - if len(spaceNames) > 0 { - for _, spaceName := range spaceNames { - groupsClaims = append(groupsClaims, fmt.Sprintf("%s:%s", orgName, spaceName)) - } - } else { - groupsClaims = append(groupsClaims, fmt.Sprintf("%s", orgName)) + groupsClaims = append(groupsClaims, fmt.Sprintf("%s", orgName)) + for _, spaceName := range spaceNames { + groupsClaims = append(groupsClaims, fmt.Sprintf("%s:%s", orgName, spaceName)) } } diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index 138dff22db..b5b581954e 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -50,13 +50,15 @@ func TestHandleCallback(t *testing.T) { expectEqual(t, err, nil) sort.Strings(identity.Groups) - expectEqual(t, len(identity.Groups), 6) - expectEqual(t, identity.Groups[0], "some-org-name-1:some-space-name-1") - expectEqual(t, identity.Groups[1], "some-org-name-2:some-space-name-2") - expectEqual(t, identity.Groups[2], "some-org-name-3") - expectEqual(t, identity.Groups[3], "some-org-name-4") - expectEqual(t, identity.Groups[4], "some-space-guid-1") - expectEqual(t, identity.Groups[5], "some-space-guid-2") + expectEqual(t, len(identity.Groups), 8) + expectEqual(t, identity.Groups[0], "some-org-name-1") + expectEqual(t, identity.Groups[1], "some-org-name-1:some-space-name-1") + expectEqual(t, identity.Groups[2], "some-org-name-2") + expectEqual(t, identity.Groups[3], "some-org-name-2:some-space-name-2") + expectEqual(t, identity.Groups[4], "some-org-name-3") + expectEqual(t, identity.Groups[5], "some-org-name-4") + expectEqual(t, identity.Groups[6], "some-space-guid-1") + expectEqual(t, identity.Groups[7], "some-space-guid-2") }) t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { From 7c7f01c30e31bed9122de25374ce4f26902535c4 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Mon, 18 Nov 2019 16:38:33 -0500 Subject: [PATCH 05/30] cf: add org guid to groups claims Co-authored-by: Rui Yang Signed-off-by: Joshua Winters --- connector/cf/cf.go | 12 +++++++++--- connector/cf/cf_test.go | 24 +++++++++++++----------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 0dd76fb821..d677a33fb3 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "net" "net/http" + "sort" "strings" "time" @@ -255,6 +256,9 @@ func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (ident for _, resource := range orgs.Resources { orgMap[resource.Metadata.Guid] = resource.Entity.Name orgSpaces[resource.Entity.Name] = []string{} + + groupsClaims = append(groupsClaims, resource.Metadata.Guid) + groupsClaims = append(groupsClaims, resource.Entity.Name) } if orgs.NextUrl != "" { @@ -292,14 +296,16 @@ func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (ident } } + var orgSpaceClaims []string for orgName, spaceNames := range orgSpaces { - groupsClaims = append(groupsClaims, fmt.Sprintf("%s", orgName)) for _, spaceName := range spaceNames { - groupsClaims = append(groupsClaims, fmt.Sprintf("%s:%s", orgName, spaceName)) + orgSpaceClaims = append(orgSpaceClaims, fmt.Sprintf("%s:%s", orgName, spaceName)) } } - identity.Groups = groupsClaims + sort.Strings(orgSpaceClaims) + + identity.Groups = append(groupsClaims, orgSpaceClaims...) } if s.OfflineAccess { diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index b5b581954e..67850d2eb2 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -7,7 +7,6 @@ import ( "net/http" "net/http/httptest" "reflect" - "sort" "strings" "testing" @@ -49,16 +48,19 @@ func TestHandleCallback(t *testing.T) { identity, err := cfConn.HandleCallback(connector.Scopes{Groups: true}, req) expectEqual(t, err, nil) - sort.Strings(identity.Groups) - expectEqual(t, len(identity.Groups), 8) - expectEqual(t, identity.Groups[0], "some-org-name-1") - expectEqual(t, identity.Groups[1], "some-org-name-1:some-space-name-1") - expectEqual(t, identity.Groups[2], "some-org-name-2") - expectEqual(t, identity.Groups[3], "some-org-name-2:some-space-name-2") - expectEqual(t, identity.Groups[4], "some-org-name-3") - expectEqual(t, identity.Groups[5], "some-org-name-4") - expectEqual(t, identity.Groups[6], "some-space-guid-1") - expectEqual(t, identity.Groups[7], "some-space-guid-2") + expectEqual(t, len(identity.Groups), 12) + expectEqual(t, identity.Groups[0], "some-org-guid-1") + expectEqual(t, identity.Groups[1], "some-org-name-1") + expectEqual(t, identity.Groups[2], "some-org-guid-2") + expectEqual(t, identity.Groups[3], "some-org-name-2") + expectEqual(t, identity.Groups[4], "some-org-guid-3") + expectEqual(t, identity.Groups[5], "some-org-name-3") + expectEqual(t, identity.Groups[6], "some-org-guid-4") + expectEqual(t, identity.Groups[7], "some-org-name-4") + expectEqual(t, identity.Groups[8], "some-space-guid-1") + expectEqual(t, identity.Groups[9], "some-space-guid-2") + expectEqual(t, identity.Groups[10], "some-org-name-1:some-space-name-1") + expectEqual(t, identity.Groups[11], "some-org-name-2:some-space-name-2") }) t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { From 87a583805472fb47639d5d639c4eca7a39b3094a Mon Sep 17 00:00:00 2001 From: Zoe Tian Date: Mon, 7 Oct 2019 17:16:00 -0400 Subject: [PATCH 06/30] add unit test and api call to `audited_spaces` and `managed_spaces` Signed-off-by: Zoe Tian Co-authored-by: Ciro S. Costa Signed-off-by: w3tian --- connector/cf/cf.go | 210 +++++++++++++++++++++++++++------------- connector/cf/cf_test.go | 143 +++++++++++++++------------ 2 files changed, 224 insertions(+), 129 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index d677a33fb3..6b33ebe1ba 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -64,6 +64,17 @@ type Entity struct { OrganizationGuid string `json:"organization_guid"` } +type Space struct { + Name string + Guid string + OrgGuid string +} + +type Org struct { + Name string + Guid string +} + func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error @@ -181,6 +192,115 @@ func (c *cfConnector) LoginURL(scopes connector.Scopes, callbackURL, state strin return oauth2Config.AuthCodeURL(state), nil } +func fetchRoleSpaces(baseUrl, path string, client *http.Client) ([]Space, error) { + var spaces []Space + + resources, err := fetchResources(baseUrl, path, client) + if err != nil { + return nil, fmt.Errorf("failed to fetch resources: %v", err) + } + + for _, resource := range resources { + spaces = append(spaces, Space{ + Name: resource.Entity.Name, + Guid: resource.Metadata.Guid, + OrgGuid: resource.Entity.OrganizationGuid, + }) + } + + return spaces, nil +} + +func fetchOrgs(baseUrl, path string, client *http.Client) ([]Org, error) { + var orgs []Org + + resources, err := fetchResources(baseUrl, path, client) + if err != nil { + return nil, fmt.Errorf("failed to fetch resources: %v", err) + } + + for _, resource := range resources { + orgs = append(orgs, Org{ + Name: resource.Entity.Name, + Guid: resource.Metadata.Guid, + }) + } + + return orgs, nil +} + +func fetchResources(baseUrl, path string, client *http.Client) ([]Resource, error) { + var ( + resources []Resource + url string + ) + + for { + url = fmt.Sprintf("%s%s", baseUrl, path) + + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to execute request: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unsuccessful status code %d", resp.StatusCode) + } + + response := CCResponse{} + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return nil, fmt.Errorf("failed to parse spaces: %v", err) + } + + resources = append(resources, response.Resources...) + + path = response.NextUrl + if path == "" { + break + } + } + + return resources, nil +} + +func getGroupsClaims(orgs []Org, spaces []Space) []string { + + var ( + orgMap = map[string]string{} + orgSpaces = map[string][]string{} + groupsClaims = map[string]bool{} + ) + + for _, org := range orgs { + orgMap[org.Guid] = org.Name + orgSpaces[org.Name] = []string{} + groupsClaims[org.Guid] = true + groupsClaims[org.Name] = true + } + + for _, space := range spaces { + orgName := orgMap[space.OrgGuid] + orgSpaces[orgName] = append(orgSpaces[orgName], space.Name) + groupsClaims[space.Guid] = true + } + + for orgName, spaceNames := range orgSpaces { + for _, spaceName := range spaceNames { + groupsClaims[fmt.Sprintf("%s:%s", orgName, spaceName)] = true + } + } + + var groups []string + for k, _ := range groupsClaims { + groups = append(groups, k) + } + + sort.Strings(groups) + + return groups +} + func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() @@ -229,83 +349,37 @@ func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (ident identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) - var orgMap = make(map[string]string) - var orgSpaces = make(map[string][]string) - var groupsClaims []string + var ( + devPath = fmt.Sprintf("/v2/users/%s/spaces", identity.UserID) + auditorPath = fmt.Sprintf("/v2/users/%s/audited_spaces", identity.UserID) + managerPath = fmt.Sprintf("/v2/users/%s/managed_spaces", identity.UserID) + orgsPath = fmt.Sprintf("/v2/users/%s/organizations", identity.UserID) + ) if s.Groups { - // fetch orgs - - var orgs CCResponse - var nextUrl = fmt.Sprintf("%s/v2/users/%s/organizations", c.apiURL, identity.UserID) - for moreResults := true; moreResults; moreResults = orgs.NextUrl != "" { - orgsResp, err := client.Get(nextUrl) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: %v", err) - } - if orgsResp.StatusCode != http.StatusOK { - return identity, fmt.Errorf("CF Connector: failed to execute request for orgs: status %d", orgsResp.StatusCode) - } - - orgs = CCResponse{} - err = json.NewDecoder(orgsResp.Body).Decode(&orgs) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to parse orgs: %v", err) - } - - for _, resource := range orgs.Resources { - orgMap[resource.Metadata.Guid] = resource.Entity.Name - orgSpaces[resource.Entity.Name] = []string{} - - groupsClaims = append(groupsClaims, resource.Metadata.Guid) - groupsClaims = append(groupsClaims, resource.Entity.Name) - } - - if orgs.NextUrl != "" { - nextUrl = fmt.Sprintf("%s%s", c.apiURL, orgs.NextUrl) - } + orgs, err := fetchOrgs(c.apiURL, orgsPath, client) + if err != nil { + return identity, fmt.Errorf("failed to fetch organizaitons: %v", err) } - // fetch spaces - var spaces CCResponse - nextUrl = fmt.Sprintf("%s/v2/users/%s/spaces", c.apiURL, identity.UserID) - for moreResults := true; moreResults; moreResults = spaces.NextUrl != "" { - spacesResp, err := client.Get(nextUrl) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: %v", err) - } - if spacesResp.StatusCode != http.StatusOK { - return identity, fmt.Errorf("CF Connector: failed to execute request for spaces: status %d", spacesResp.StatusCode) - } - - spaces = CCResponse{} - err = json.NewDecoder(spacesResp.Body).Decode(&spaces) - if err != nil { - return identity, fmt.Errorf("CF Connector: failed to parse spaces: %v", err) - } - - for _, resource := range spaces.Resources { - orgName := orgMap[resource.Entity.OrganizationGuid] - orgSpaces[orgName] = append(orgSpaces[orgName], resource.Entity.Name) - - groupsClaims = append(groupsClaims, resource.Metadata.Guid) - } - - if spaces.NextUrl != "" { - nextUrl = fmt.Sprintf("%s%s", c.apiURL, spaces.NextUrl) - } + developerSpaces, err := fetchRoleSpaces(c.apiURL, devPath, client) + if err != nil { + return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) } - var orgSpaceClaims []string - for orgName, spaceNames := range orgSpaces { - for _, spaceName := range spaceNames { - orgSpaceClaims = append(orgSpaceClaims, fmt.Sprintf("%s:%s", orgName, spaceName)) - } + auditorSpaces, err := fetchRoleSpaces(c.apiURL, auditorPath, client) + if err != nil { + return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) + } + + managerSpaces, err := fetchRoleSpaces(c.apiURL, managerPath, client) + if err != nil { + return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) } - sort.Strings(orgSpaceClaims) + spaces := append(developerSpaces, append(auditorSpaces, managerSpaces...)...) - identity.Groups = append(groupsClaims, orgSpaceClaims...) + identity.Groups = getGroupsClaims(orgs, spaces) } if s.OfflineAccess { diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index 67850d2eb2..40daa7c758 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -50,17 +50,17 @@ func TestHandleCallback(t *testing.T) { expectEqual(t, len(identity.Groups), 12) expectEqual(t, identity.Groups[0], "some-org-guid-1") - expectEqual(t, identity.Groups[1], "some-org-name-1") - expectEqual(t, identity.Groups[2], "some-org-guid-2") - expectEqual(t, identity.Groups[3], "some-org-name-2") - expectEqual(t, identity.Groups[4], "some-org-guid-3") - expectEqual(t, identity.Groups[5], "some-org-name-3") - expectEqual(t, identity.Groups[6], "some-org-guid-4") - expectEqual(t, identity.Groups[7], "some-org-name-4") - expectEqual(t, identity.Groups[8], "some-space-guid-1") - expectEqual(t, identity.Groups[9], "some-space-guid-2") - expectEqual(t, identity.Groups[10], "some-org-name-1:some-space-name-1") - expectEqual(t, identity.Groups[11], "some-org-name-2:some-space-name-2") + expectEqual(t, identity.Groups[1], "some-org-guid-2") + expectEqual(t, identity.Groups[2], "some-org-guid-3") + expectEqual(t, identity.Groups[3], "some-org-guid-4") + expectEqual(t, identity.Groups[4], "some-org-name-1") + expectEqual(t, identity.Groups[5], "some-org-name-1:some-space-name-1") + expectEqual(t, identity.Groups[6], "some-org-name-2") + expectEqual(t, identity.Groups[7], "some-org-name-2:some-space-name-2") + expectEqual(t, identity.Groups[8], "some-org-name-3") + expectEqual(t, identity.Groups[9], "some-org-name-4") + expectEqual(t, identity.Groups[10], "some-space-guid-1") + expectEqual(t, identity.Groups[11], "some-space-guid-2") }) t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { @@ -85,6 +85,64 @@ func TestHandleCallback(t *testing.T) { }) } +func testSpaceHandler(reqUrl, spaceApiEndpoint string) (result map[string]interface{}) { + fullUrl := fmt.Sprintf("%s?order-direction=asc&page=2&results-per-page=50", spaceApiEndpoint) + if strings.Contains(reqUrl, fullUrl) { + result = map[string]interface{}{ + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-space-guid-2"}, + "entity": map[string]string{"name": "some-space-name-2", "organization_guid": "some-org-guid-2"}, + }, + }, + } + } else { + nextUrl := fmt.Sprintf("/v2/users/12345/%s?order-direction=asc&page=2&results-per-page=50", spaceApiEndpoint) + result = map[string]interface{}{ + "next_url": nextUrl, + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-space-guid-1"}, + "entity": map[string]string{"name": "some-space-name-1", "organization_guid": "some-org-guid-1"}, + }, + }, + } + } + return result +} + +func testOrgHandler(reqUrl string) (result map[string]interface{}) { + if strings.Contains(reqUrl, "organizations?order-direction=asc&page=2&results-per-page=50") { + result = map[string]interface{}{ + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-org-guid-3"}, + "entity": map[string]string{"name": "some-org-name-3"}, + }, + { + "metadata": map[string]string{"guid": "some-org-guid-4"}, + "entity": map[string]string{"name": "some-org-name-4"}, + }, + }, + } + } else { + result = map[string]interface{}{ + "next_url": "/v2/users/12345/organizations?order-direction=asc&page=2&results-per-page=50", + "resources": []map[string]interface{}{ + { + "metadata": map[string]string{"guid": "some-org-guid-1"}, + "entity": map[string]string{"name": "some-org-name-1"}, + }, + { + "metadata": map[string]string{"guid": "some-org-guid-2"}, + "entity": map[string]string{"name": "some-org-name-2"}, + }, + }, + } + } + return result +} + func testSetup() *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { @@ -127,58 +185,21 @@ func testSetup() *httptest.Server { mux.HandleFunc("/v2/users/", func(w http.ResponseWriter, r *http.Request) { var result map[string]interface{} - if strings.Contains(r.URL.String(), "spaces") { - if strings.Contains(r.URL.String(), "spaces?order-direction=asc&page=2&results-per-page=50") { - result = map[string]interface{}{ - "resources": []map[string]interface{}{ - { - "metadata": map[string]string{"guid": "some-space-guid-2"}, - "entity": map[string]string{"name": "some-space-name-2", "organization_guid": "some-org-guid-2"}, - }, - }, - } - } else { - result = map[string]interface{}{ - "next_url": "/v2/users/12345/spaces?order-direction=asc&page=2&results-per-page=50", - "resources": []map[string]interface{}{ - { - "metadata": map[string]string{"guid": "some-space-guid-1"}, - "entity": map[string]string{"name": "some-space-name-1", "organization_guid": "some-org-guid-1"}, - }, - }, - } - } + reqUrl := r.URL.String() + if strings.Contains(reqUrl, "/spaces") { + result = testSpaceHandler(reqUrl, "spaces") + } + + if strings.Contains(reqUrl, "/audited_spaces") { + result = testSpaceHandler(reqUrl, "audited_spaces") + } + + if strings.Contains(reqUrl, "/managed_spaces") { + result = testSpaceHandler(reqUrl, "managed_spaces") } - if strings.Contains(r.URL.String(), "organizations") { - if strings.Contains(r.URL.String(), "organizations?order-direction=asc&page=2&results-per-page=50") { - result = map[string]interface{}{ - "resources": []map[string]interface{}{ - { - "metadata": map[string]string{"guid": "some-org-guid-3"}, - "entity": map[string]string{"name": "some-org-name-3"}, - }, - { - "metadata": map[string]string{"guid": "some-org-guid-4"}, - "entity": map[string]string{"name": "some-org-name-4"}, - }, - }, - } - } else { - result = map[string]interface{}{ - "next_url": "/v2/users/12345/organizations?order-direction=asc&page=2&results-per-page=50", - "resources": []map[string]interface{}{ - { - "metadata": map[string]string{"guid": "some-org-guid-1"}, - "entity": map[string]string{"name": "some-org-name-1"}, - }, - { - "metadata": map[string]string{"guid": "some-org-guid-2"}, - "entity": map[string]string{"name": "some-org-name-2"}, - }, - }, - } - } + if strings.Contains(reqUrl, "organizations") { + result = testOrgHandler(reqUrl) } json.NewEncoder(w).Encode(result) From 3dbed761a65529e4af4b25df092a2230d0280384 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 4 Nov 2019 17:06:23 -0500 Subject: [PATCH 07/30] append role to space guids Signed-off-by: Rui Yang Co-authored-by: Joshua Winters --- connector/cf/cf.go | 27 +++++++++++++++------------ connector/cf/cf_test.go | 10 ++++++++-- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 6b33ebe1ba..67c3d56702 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -68,6 +68,7 @@ type Space struct { Name string Guid string OrgGuid string + Role string } type Org struct { @@ -192,7 +193,7 @@ func (c *cfConnector) LoginURL(scopes connector.Scopes, callbackURL, state strin return oauth2Config.AuthCodeURL(state), nil } -func fetchRoleSpaces(baseUrl, path string, client *http.Client) ([]Space, error) { +func fetchRoleSpaces(baseUrl, path, role string, client *http.Client) ([]Space, error) { var spaces []Space resources, err := fetchResources(baseUrl, path, client) @@ -205,6 +206,7 @@ func fetchRoleSpaces(baseUrl, path string, client *http.Client) ([]Space, error) Name: resource.Entity.Name, Guid: resource.Metadata.Guid, OrgGuid: resource.Entity.OrganizationGuid, + Role: role, }) } @@ -268,32 +270,33 @@ func getGroupsClaims(orgs []Org, spaces []Space) []string { var ( orgMap = map[string]string{} - orgSpaces = map[string][]string{} + orgSpaces = map[string][]Space{} groupsClaims = map[string]bool{} ) for _, org := range orgs { orgMap[org.Guid] = org.Name - orgSpaces[org.Name] = []string{} + orgSpaces[org.Name] = []Space{} groupsClaims[org.Guid] = true groupsClaims[org.Name] = true } for _, space := range spaces { orgName := orgMap[space.OrgGuid] - orgSpaces[orgName] = append(orgSpaces[orgName], space.Name) + orgSpaces[orgName] = append(orgSpaces[orgName], space) groupsClaims[space.Guid] = true + groupsClaims[fmt.Sprintf("%s:%s", space.Guid, space.Role)] = true } - for orgName, spaceNames := range orgSpaces { - for _, spaceName := range spaceNames { - groupsClaims[fmt.Sprintf("%s:%s", orgName, spaceName)] = true + for orgName, spaces := range orgSpaces { + for _, space := range spaces { + groupsClaims[fmt.Sprintf("%s:%s", orgName, space.Name)] = true } } var groups []string - for k, _ := range groupsClaims { - groups = append(groups, k) + for group, _ := range groupsClaims { + groups = append(groups, group) } sort.Strings(groups) @@ -362,17 +365,17 @@ func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (ident return identity, fmt.Errorf("failed to fetch organizaitons: %v", err) } - developerSpaces, err := fetchRoleSpaces(c.apiURL, devPath, client) + developerSpaces, err := fetchRoleSpaces(c.apiURL, devPath, "developer", client) if err != nil { return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) } - auditorSpaces, err := fetchRoleSpaces(c.apiURL, auditorPath, client) + auditorSpaces, err := fetchRoleSpaces(c.apiURL, auditorPath, "auditor", client) if err != nil { return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) } - managerSpaces, err := fetchRoleSpaces(c.apiURL, managerPath, client) + managerSpaces, err := fetchRoleSpaces(c.apiURL, managerPath, "manager", client) if err != nil { return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) } diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index 40daa7c758..f6014230b4 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -48,7 +48,7 @@ func TestHandleCallback(t *testing.T) { identity, err := cfConn.HandleCallback(connector.Scopes{Groups: true}, req) expectEqual(t, err, nil) - expectEqual(t, len(identity.Groups), 12) + expectEqual(t, len(identity.Groups), 18) expectEqual(t, identity.Groups[0], "some-org-guid-1") expectEqual(t, identity.Groups[1], "some-org-guid-2") expectEqual(t, identity.Groups[2], "some-org-guid-3") @@ -60,7 +60,13 @@ func TestHandleCallback(t *testing.T) { expectEqual(t, identity.Groups[8], "some-org-name-3") expectEqual(t, identity.Groups[9], "some-org-name-4") expectEqual(t, identity.Groups[10], "some-space-guid-1") - expectEqual(t, identity.Groups[11], "some-space-guid-2") + expectEqual(t, identity.Groups[11], "some-space-guid-1:auditor") + expectEqual(t, identity.Groups[12], "some-space-guid-1:developer") + expectEqual(t, identity.Groups[13], "some-space-guid-1:manager") + expectEqual(t, identity.Groups[14], "some-space-guid-2") + expectEqual(t, identity.Groups[15], "some-space-guid-2:auditor") + expectEqual(t, identity.Groups[16], "some-space-guid-2:developer") + expectEqual(t, identity.Groups[17], "some-space-guid-2:manager") }) t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { From 21bb12846b27564344d2c44bd58b4f89dea4bc65 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Mon, 25 Nov 2019 15:15:30 -0500 Subject: [PATCH 08/30] add cf org:space:role group claim to token Signed-off-by: Joshua Winters Co-authored-by: Rui Yang --- connector/cf/cf.go | 1 + connector/cf/cf_test.go | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 67c3d56702..0dcbb3a86f 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -291,6 +291,7 @@ func getGroupsClaims(orgs []Org, spaces []Space) []string { for orgName, spaces := range orgSpaces { for _, space := range spaces { groupsClaims[fmt.Sprintf("%s:%s", orgName, space.Name)] = true + groupsClaims[fmt.Sprintf("%s:%s:%s", orgName, space.Name, space.Role)] = true } } diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index f6014230b4..afc273daa1 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -48,25 +48,31 @@ func TestHandleCallback(t *testing.T) { identity, err := cfConn.HandleCallback(connector.Scopes{Groups: true}, req) expectEqual(t, err, nil) - expectEqual(t, len(identity.Groups), 18) + expectEqual(t, len(identity.Groups), 24) expectEqual(t, identity.Groups[0], "some-org-guid-1") expectEqual(t, identity.Groups[1], "some-org-guid-2") expectEqual(t, identity.Groups[2], "some-org-guid-3") expectEqual(t, identity.Groups[3], "some-org-guid-4") expectEqual(t, identity.Groups[4], "some-org-name-1") expectEqual(t, identity.Groups[5], "some-org-name-1:some-space-name-1") - expectEqual(t, identity.Groups[6], "some-org-name-2") - expectEqual(t, identity.Groups[7], "some-org-name-2:some-space-name-2") - expectEqual(t, identity.Groups[8], "some-org-name-3") - expectEqual(t, identity.Groups[9], "some-org-name-4") - expectEqual(t, identity.Groups[10], "some-space-guid-1") - expectEqual(t, identity.Groups[11], "some-space-guid-1:auditor") - expectEqual(t, identity.Groups[12], "some-space-guid-1:developer") - expectEqual(t, identity.Groups[13], "some-space-guid-1:manager") - expectEqual(t, identity.Groups[14], "some-space-guid-2") - expectEqual(t, identity.Groups[15], "some-space-guid-2:auditor") - expectEqual(t, identity.Groups[16], "some-space-guid-2:developer") - expectEqual(t, identity.Groups[17], "some-space-guid-2:manager") + expectEqual(t, identity.Groups[6], "some-org-name-1:some-space-name-1:auditor") + expectEqual(t, identity.Groups[7], "some-org-name-1:some-space-name-1:developer") + expectEqual(t, identity.Groups[8], "some-org-name-1:some-space-name-1:manager") + expectEqual(t, identity.Groups[9], "some-org-name-2") + expectEqual(t, identity.Groups[10], "some-org-name-2:some-space-name-2") + expectEqual(t, identity.Groups[11], "some-org-name-2:some-space-name-2:auditor") + expectEqual(t, identity.Groups[12], "some-org-name-2:some-space-name-2:developer") + expectEqual(t, identity.Groups[13], "some-org-name-2:some-space-name-2:manager") + expectEqual(t, identity.Groups[14], "some-org-name-3") + expectEqual(t, identity.Groups[15], "some-org-name-4") + expectEqual(t, identity.Groups[16], "some-space-guid-1") + expectEqual(t, identity.Groups[17], "some-space-guid-1:auditor") + expectEqual(t, identity.Groups[18], "some-space-guid-1:developer") + expectEqual(t, identity.Groups[19], "some-space-guid-1:manager") + expectEqual(t, identity.Groups[20], "some-space-guid-2") + expectEqual(t, identity.Groups[21], "some-space-guid-2:auditor") + expectEqual(t, identity.Groups[22], "some-space-guid-2:developer") + expectEqual(t, identity.Groups[23], "some-space-guid-2:manager") }) t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { From 78e936f22246cd8fb7cbab51ec2c1188d51e8509 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 13 Jan 2020 13:19:53 -0500 Subject: [PATCH 09/30] fix lint errors gofumpt-ed Signed-off-by: Rui Yang --- connector/cf/cf.go | 59 +++++++++++++++++++---------------------- connector/cf/cf_test.go | 37 +++++++++++++------------- 2 files changed, 46 insertions(+), 50 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 0dcbb3a86f..ba0b09d1fb 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -14,9 +14,10 @@ import ( "strings" "time" + "golang.org/x/oauth2" + "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/log" - "golang.org/x/oauth2" ) type cfConnector struct { @@ -45,7 +46,7 @@ type Config struct { } type CCResponse struct { - NextUrl string `json:"next_url"` + NextURL string `json:"next_url"` Resources []Resource `json:"resources"` TotalResults int `json:"total_results"` } @@ -56,24 +57,24 @@ type Resource struct { } type Metadata struct { - Guid string `json:"guid"` + GUID string `json:"guid"` } type Entity struct { Name string `json:"name"` - OrganizationGuid string `json:"organization_guid"` + OrganizationGUID string `json:"organization_guid"` } type Space struct { Name string - Guid string - OrgGuid string + GUID string + OrgGUID string Role string } type Org struct { Name string - Guid string + GUID string } func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { @@ -94,7 +95,6 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) apiURL := strings.TrimRight(c.APIURL, "/") apiResp, err := cfConn.httpClient.Get(fmt.Sprintf("%s/v2/info", apiURL)) - if err != nil { logger.Errorf("failed-to-send-request-to-cloud-controller-api", err) return nil, err @@ -103,7 +103,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) defer apiResp.Body.Close() if apiResp.StatusCode != http.StatusOK { - err = errors.New(fmt.Sprintf("request failed with status %d", apiResp.StatusCode)) + err = fmt.Errorf("request failed with status %d", apiResp.StatusCode) logger.Errorf("failed-get-info-response-from-api", err) return nil, err } @@ -113,15 +113,14 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) uaaURL := strings.TrimRight(apiResult["authorization_endpoint"].(string), "/") uaaResp, err := cfConn.httpClient.Get(fmt.Sprintf("%s/.well-known/openid-configuration", uaaURL)) - if err != nil { logger.Errorf("failed-to-send-request-to-uaa-api", err) return nil, err } if apiResp.StatusCode != http.StatusOK { - err = errors.New(fmt.Sprintf("request failed with status %d", apiResp.StatusCode)) - logger.Errorf("failed-to-get-well-known-config-repsonse-from-api", err) + err = fmt.Errorf("request failed with status %d", apiResp.StatusCode) + logger.Errorf("failed-to-get-well-known-config-response-from-api", err) return nil, err } @@ -177,7 +176,6 @@ func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, err } func (c *cfConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { - if c.redirectURI != callbackURL { return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } @@ -193,10 +191,10 @@ func (c *cfConnector) LoginURL(scopes connector.Scopes, callbackURL, state strin return oauth2Config.AuthCodeURL(state), nil } -func fetchRoleSpaces(baseUrl, path, role string, client *http.Client) ([]Space, error) { +func fetchRoleSpaces(baseURL, path, role string, client *http.Client) ([]Space, error) { var spaces []Space - resources, err := fetchResources(baseUrl, path, client) + resources, err := fetchResources(baseURL, path, client) if err != nil { return nil, fmt.Errorf("failed to fetch resources: %v", err) } @@ -204,8 +202,8 @@ func fetchRoleSpaces(baseUrl, path, role string, client *http.Client) ([]Space, for _, resource := range resources { spaces = append(spaces, Space{ Name: resource.Entity.Name, - Guid: resource.Metadata.Guid, - OrgGuid: resource.Entity.OrganizationGuid, + GUID: resource.Metadata.GUID, + OrgGUID: resource.Entity.OrganizationGUID, Role: role, }) } @@ -213,10 +211,10 @@ func fetchRoleSpaces(baseUrl, path, role string, client *http.Client) ([]Space, return spaces, nil } -func fetchOrgs(baseUrl, path string, client *http.Client) ([]Org, error) { +func fetchOrgs(baseURL, path string, client *http.Client) ([]Org, error) { var orgs []Org - resources, err := fetchResources(baseUrl, path, client) + resources, err := fetchResources(baseURL, path, client) if err != nil { return nil, fmt.Errorf("failed to fetch resources: %v", err) } @@ -224,26 +222,27 @@ func fetchOrgs(baseUrl, path string, client *http.Client) ([]Org, error) { for _, resource := range resources { orgs = append(orgs, Org{ Name: resource.Entity.Name, - Guid: resource.Metadata.Guid, + GUID: resource.Metadata.GUID, }) } return orgs, nil } -func fetchResources(baseUrl, path string, client *http.Client) ([]Resource, error) { +func fetchResources(baseURL, path string, client *http.Client) ([]Resource, error) { var ( resources []Resource url string ) for { - url = fmt.Sprintf("%s%s", baseUrl, path) + url = fmt.Sprintf("%s%s", baseURL, path) resp, err := client.Get(url) if err != nil { return nil, fmt.Errorf("failed to execute request: %v", err) } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unsuccessful status code %d", resp.StatusCode) @@ -257,7 +256,7 @@ func fetchResources(baseUrl, path string, client *http.Client) ([]Resource, erro resources = append(resources, response.Resources...) - path = response.NextUrl + path = response.NextURL if path == "" { break } @@ -267,7 +266,6 @@ func fetchResources(baseUrl, path string, client *http.Client) ([]Resource, erro } func getGroupsClaims(orgs []Org, spaces []Space) []string { - var ( orgMap = map[string]string{} orgSpaces = map[string][]Space{} @@ -275,17 +273,17 @@ func getGroupsClaims(orgs []Org, spaces []Space) []string { ) for _, org := range orgs { - orgMap[org.Guid] = org.Name + orgMap[org.GUID] = org.Name orgSpaces[org.Name] = []Space{} - groupsClaims[org.Guid] = true + groupsClaims[org.GUID] = true groupsClaims[org.Name] = true } for _, space := range spaces { - orgName := orgMap[space.OrgGuid] + orgName := orgMap[space.OrgGUID] orgSpaces[orgName] = append(orgSpaces[orgName], space) - groupsClaims[space.Guid] = true - groupsClaims[fmt.Sprintf("%s:%s", space.Guid, space.Role)] = true + groupsClaims[space.GUID] = true + groupsClaims[fmt.Sprintf("%s:%s", space.GUID, space.Role)] = true } for orgName, spaces := range orgSpaces { @@ -296,7 +294,7 @@ func getGroupsClaims(orgs []Org, spaces []Space) []string { } var groups []string - for group, _ := range groupsClaims { + for group := range groupsClaims { groups = append(groups, group) } @@ -306,7 +304,6 @@ func getGroupsClaims(orgs []Org, spaces []Space) []string { } func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { - q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, errors.New(q.Get("error_description")) diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index afc273daa1..b9bf68dbeb 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -10,8 +10,9 @@ import ( "strings" "testing" - "github.com/dexidp/dex/connector" "github.com/sirupsen/logrus" + + "github.com/dexidp/dex/connector" ) func TestOpen(t *testing.T) { @@ -26,7 +27,6 @@ func TestOpen(t *testing.T) { } func TestHandleCallback(t *testing.T) { - testServer := testSetup() defer testServer.Close() @@ -97,9 +97,9 @@ func TestHandleCallback(t *testing.T) { }) } -func testSpaceHandler(reqUrl, spaceApiEndpoint string) (result map[string]interface{}) { - fullUrl := fmt.Sprintf("%s?order-direction=asc&page=2&results-per-page=50", spaceApiEndpoint) - if strings.Contains(reqUrl, fullUrl) { +func testSpaceHandler(reqURL, spaceAPIEndpoint string) (result map[string]interface{}) { + fullURL := fmt.Sprintf("%s?order-direction=asc&page=2&results-per-page=50", spaceAPIEndpoint) + if strings.Contains(reqURL, fullURL) { result = map[string]interface{}{ "resources": []map[string]interface{}{ { @@ -109,9 +109,9 @@ func testSpaceHandler(reqUrl, spaceApiEndpoint string) (result map[string]interf }, } } else { - nextUrl := fmt.Sprintf("/v2/users/12345/%s?order-direction=asc&page=2&results-per-page=50", spaceApiEndpoint) + nextURL := fmt.Sprintf("/v2/users/12345/%s?order-direction=asc&page=2&results-per-page=50", spaceAPIEndpoint) result = map[string]interface{}{ - "next_url": nextUrl, + "next_url": nextURL, "resources": []map[string]interface{}{ { "metadata": map[string]string{"guid": "some-space-guid-1"}, @@ -123,8 +123,8 @@ func testSpaceHandler(reqUrl, spaceApiEndpoint string) (result map[string]interf return result } -func testOrgHandler(reqUrl string) (result map[string]interface{}) { - if strings.Contains(reqUrl, "organizations?order-direction=asc&page=2&results-per-page=50") { +func testOrgHandler(reqURL string) (result map[string]interface{}) { + if strings.Contains(reqURL, "organizations?order-direction=asc&page=2&results-per-page=50") { result = map[string]interface{}{ "resources": []map[string]interface{}{ { @@ -197,21 +197,21 @@ func testSetup() *httptest.Server { mux.HandleFunc("/v2/users/", func(w http.ResponseWriter, r *http.Request) { var result map[string]interface{} - reqUrl := r.URL.String() - if strings.Contains(reqUrl, "/spaces") { - result = testSpaceHandler(reqUrl, "spaces") + reqURL := r.URL.String() + if strings.Contains(reqURL, "/spaces") { + result = testSpaceHandler(reqURL, "spaces") } - if strings.Contains(reqUrl, "/audited_spaces") { - result = testSpaceHandler(reqUrl, "audited_spaces") + if strings.Contains(reqURL, "/audited_spaces") { + result = testSpaceHandler(reqURL, "audited_spaces") } - if strings.Contains(reqUrl, "/managed_spaces") { - result = testSpaceHandler(reqUrl, "managed_spaces") + if strings.Contains(reqURL, "/managed_spaces") { + result = testSpaceHandler(reqURL, "managed_spaces") } - if strings.Contains(reqUrl, "organizations") { - result = testOrgHandler(reqUrl) + if strings.Contains(reqURL, "organizations") { + result = testOrgHandler(reqURL) } json.NewEncoder(w).Encode(result) @@ -221,7 +221,6 @@ func testSetup() *httptest.Server { } func newConnector(t *testing.T, serverURL string) *cfConnector { - callBackURL := fmt.Sprintf("%s/callback", serverURL) testConfig := Config{ From 73af7c02fb062623c31adc1c27c4bb0eff7d83b0 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Fri, 5 Mar 2021 12:40:56 -0500 Subject: [PATCH 10/30] run golangcli-lint Signed-off-by: Rui Yang --- connector/cf/cf.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index ba0b09d1fb..4d839ff63a 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -192,38 +192,36 @@ func (c *cfConnector) LoginURL(scopes connector.Scopes, callbackURL, state strin } func fetchRoleSpaces(baseURL, path, role string, client *http.Client) ([]Space, error) { - var spaces []Space - resources, err := fetchResources(baseURL, path, client) if err != nil { return nil, fmt.Errorf("failed to fetch resources: %v", err) } - for _, resource := range resources { - spaces = append(spaces, Space{ + spaces := make([]Space, len(resources)) + for i, resource := range resources { + spaces[i] = Space{ Name: resource.Entity.Name, GUID: resource.Metadata.GUID, OrgGUID: resource.Entity.OrganizationGUID, Role: role, - }) + } } return spaces, nil } func fetchOrgs(baseURL, path string, client *http.Client) ([]Org, error) { - var orgs []Org - resources, err := fetchResources(baseURL, path, client) if err != nil { return nil, fmt.Errorf("failed to fetch resources: %v", err) } - for _, resource := range resources { - orgs = append(orgs, Org{ + orgs := make([]Org, len(resources)) + for i, resource := range resources { + orgs[i] = Org{ Name: resource.Entity.Name, GUID: resource.Metadata.GUID, - }) + } } return orgs, nil @@ -293,7 +291,7 @@ func getGroupsClaims(orgs []Org, spaces []Space) []string { } } - var groups []string + groups := make([]string, 0, len(groupsClaims)) for group := range groupsClaims { groups = append(groups, group) } From 09330cf288d43679e9025c6faefc1c5cd5f18ad3 Mon Sep 17 00:00:00 2001 From: Josh Winters Date: Fri, 17 Apr 2020 16:27:02 -0400 Subject: [PATCH 11/30] use bcrypt when comparing client secrets - this assumes that the client is already bcrytped when passed to dex. Similar to user passwords. Signed-off-by: Josh Winters Co-authored-by: Vikram Yadav --- server/handlers.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/handlers.go b/server/handlers.go index eb65f490bd..db835997b1 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -16,6 +16,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" jose "gopkg.in/square/go-jose.v2" "github.com/dexidp/dex/connector" @@ -679,12 +680,21 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { } return } + if client.Secret != clientSecret { if clientSecret == "" { s.logger.Infof("missing client_secret on token request for client: %s", client.ID) } else { s.logger.Infof("invalid client_secret on token request for client: %s", client.ID) } + } + + if err := checkCost([]byte(client.Secret)); err != nil { + s.logger.Errorf("failed to check cost of client secret: %v", err) + s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) + return + } + if err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret)); err != nil { s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized) return } From cbe0a174273d801e83b75c84bd40b693848bd1ce Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Fri, 25 Sep 2020 11:59:42 -0400 Subject: [PATCH 12/30] add dex config flag for enabling client secret encryption * if enabled, it will make sure client secret is bcrypted correctly * if not, it falls back to old behaviour that allowing empty client secret and comparing plain text, though now it will do ConstantTimeCompare to avoid a timing attack. So in either way it should provide more secure of client secret verification. Co-authored-by: Alex Surraci Signed-off-by: Rui Yang --- server/handlers.go | 30 ++++---- server/server.go | 25 +++++++ server/server_test.go | 160 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 15 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index db835997b1..494af23298 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -2,6 +2,7 @@ package server import ( "crypto/sha256" + "crypto/subtle" "encoding/base64" "encoding/json" "errors" @@ -681,22 +682,21 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { return } - if client.Secret != clientSecret { - if clientSecret == "" { - s.logger.Infof("missing client_secret on token request for client: %s", client.ID) - } else { - s.logger.Infof("invalid client_secret on token request for client: %s", client.ID) + if s.hashClientSecret { + if err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret)); err != nil { + s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized) + return + } + } else { + if subtle.ConstantTimeCompare([]byte(client.Secret), []byte(clientSecret)) != 1 { + if clientSecret == "" { + s.logger.Infof("missing client_secret on token request for client: %s", client.ID) + } else { + s.logger.Infof("invalid client_secret on token request for client: %s", client.ID) + } + s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized) + return } - } - - if err := checkCost([]byte(client.Secret)); err != nil { - s.logger.Errorf("failed to check cost of client secret: %v", err) - s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) - return - } - if err := bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret)); err != nil { - s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized) - return } grantType := r.PostFormValue("grant_type") diff --git a/server/server.go b/server/server.go index a79b7cfd3b..5909d5c350 100644 --- a/server/server.go +++ b/server/server.go @@ -77,6 +77,9 @@ type Config struct { // If enabled, the connectors selection page will always be shown even if there's only one AlwaysShowLoginScreen bool + // If enabled, the client secret is expected to be encrypted + HashClientSecret bool + RotateKeysAfter time.Duration // Defaults to 6 hours. IDTokensValidFor time.Duration // Defaults to 24 hours AuthRequestsValidFor time.Duration // Defaults to 24 hours @@ -151,6 +154,9 @@ type Server struct { // If enabled, show the connector selection screen even if there's only one alwaysShowLogin bool + // If enabled, the client secret is expected to be encrypted + hashClientSecret bool + // Used for password grant passwordConnector string @@ -189,6 +195,24 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) if c.Storage == nil { return nil, errors.New("server: storage cannot be nil") } + + if c.HashClientSecret { + clients, err := c.Storage.ListClients() + if err != nil { + return nil, fmt.Errorf("server: failed to list clients") + } + + for _, client := range clients { + if client.Secret == "" { + return nil, fmt.Errorf("server: client secret can't be empty") + } + + if err = checkCost([]byte(client.Secret)); err != nil { + return nil, fmt.Errorf("server: failed to check cost of client secret: %v", err) + } + } + } + if len(c.SupportedResponseTypes) == 0 { c.SupportedResponseTypes = []string{responseTypeCode} } @@ -232,6 +256,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) deviceRequestsValidFor: value(c.DeviceRequestsValidFor, 5*time.Minute), skipApproval: c.SkipApprovalScreen, alwaysShowLogin: c.AlwaysShowLoginScreen, + hashClientSecret: c.HashClientSecret, now: now, templates: tmpls, passwordConnector: c.PasswordConnector, diff --git a/server/server_test.go b/server/server_test.go index 87ca6c171e..cbb298e58a 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1637,3 +1637,163 @@ func TestOAuth2DeviceFlow(t *testing.T) { }() } } + +func TestClientSecretEncryption(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + httpServer, s := newTestServer(ctx, t, func(c *Config) { + c.HashClientSecret = true + }) + defer httpServer.Close() + + clientID := "testclient" + clientSecret := "testclientsecret" + hash, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("failed to bcrypt: %s", err) + } + + // Query server's provider metadata. + p, err := oidc.NewProvider(ctx, httpServer.URL) + if err != nil { + t.Fatalf("failed to get provider: %v", err) + } + + var ( + // If the OAuth2 client didn't get a response, we need + // to print the requests the user saw. + gotCode bool + reqDump, respDump []byte // Auth step, not token. + state = "a_state" + ) + defer func() { + if !gotCode { + t.Errorf("never got a code in callback\n%s\n%s", reqDump, respDump) + } + }() + + // Setup OAuth2 client. + var oauth2Config *oauth2.Config + + requestedScopes := []string{oidc.ScopeOpenID, "email", "profile", "groups", "offline_access"} + + // Create the OAuth2 config. + oauth2Config = &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + Endpoint: p.Endpoint(), + Scopes: requestedScopes, + } + + oauth2Client := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/callback" { + // User is visiting app first time. Redirect to dex. + http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusSeeOther) + return + } + + // User is at '/callback' so they were just redirected _from_ dex. + q := r.URL.Query() + + // Grab code, exchange for token. + if code := q.Get("code"); code != "" { + gotCode = true + token, err := oauth2Config.Exchange(ctx, code) + if err != nil { + t.Errorf("failed to exchange code for token: %v", err) + return + } + + oidcConfig := &oidc.Config{SkipClientIDCheck: true} + + idToken, ok := token.Extra("id_token").(string) + if !ok { + t.Errorf("no id token found") + return + } + if _, err := p.Verifier(oidcConfig).Verify(ctx, idToken); err != nil { + t.Errorf("failed to verify id token: %v", err) + return + } + } + + w.WriteHeader(http.StatusOK) + })) + + oauth2Config.RedirectURL = oauth2Client.URL + "/callback" + + defer oauth2Client.Close() + + // Regester the client above with dex. + client := storage.Client{ + ID: clientID, + Secret: string(hash), + RedirectURIs: []string{oauth2Client.URL + "/callback"}, + } + if err := s.storage.CreateClient(client); err != nil { + t.Fatalf("failed to create client: %v", err) + } + + // Login! + // + // 1. First request to client, redirects to dex. + // 2. Dex "logs in" the user, redirects to client with "code". + // 3. Client exchanges "code" for "token" (id_token, refresh_token, etc.). + // 4. Test is run with OAuth2 token response. + // + resp, err := http.Get(oauth2Client.URL + "/login") + if err != nil { + t.Fatalf("get failed: %v", err) + } + defer resp.Body.Close() + + if reqDump, err = httputil.DumpRequest(resp.Request, false); err != nil { + t.Fatal(err) + } + if respDump, err = httputil.DumpResponse(resp, true); err != nil { + t.Fatal(err) + } +} + +func TestClientSecretEncryptionCost(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + clientID := "testclient" + clientSecret := "testclientsecret" + hash, err := bcrypt.GenerateFromPassword([]byte(clientSecret), 5) + if err != nil { + t.Fatalf("failed to bcrypt: %s", err) + } + + // Register the client above with dex. + client := storage.Client{ + ID: clientID, + Secret: string(hash), + } + + config := Config{ + Storage: memory.New(logger), + Web: WebConfig{ + Dir: "../web", + }, + Logger: logger, + PrometheusRegistry: prometheus.NewRegistry(), + HashClientSecret: true, + } + + err = config.Storage.CreateClient(client) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + _, err = newServer(ctx, config, staticRotationStrategy(testKey)) + if err == nil { + t.Error("constructing server should have failed") + } + + if !strings.Contains(err.Error(), "failed to check cost") { + t.Error("should have failed with cost error") + } +} From 25477232b7024e036226d611a86b67b3c08cf697 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Fri, 4 May 2018 12:43:09 -0400 Subject: [PATCH 13/30] Add generic oauth connector Co-authored-by: Shash Reddy Signed-off-by: Joshua Winters --- connector/oauth/oauth.go | 242 ++++++++++++++++++++++++++++++++++ connector/oauth/oauth_test.go | 234 ++++++++++++++++++++++++++++++++ server/server.go | 2 + 3 files changed, 478 insertions(+) create mode 100644 connector/oauth/oauth.go create mode 100644 connector/oauth/oauth_test.go diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go new file mode 100644 index 0000000000..7bf480cdc5 --- /dev/null +++ b/connector/oauth/oauth.go @@ -0,0 +1,242 @@ +package oauth + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "strings" + "time" + + "github.com/dexidp/dex/connector" + "github.com/dexidp/dex/pkg/log" + "golang.org/x/oauth2" +) + +type oauthConnector struct { + clientID string + clientSecret string + redirectURI string + tokenURL string + authorizationURL string + userInfoURL string + scopes []string + groupsKey string + httpClient *http.Client + logger log.Logger +} + +type connectorData struct { + AccessToken string +} + +type Config struct { + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + TokenURL string `json:"tokenURL"` + AuthorizationURL string `json:"authorizationURL"` + UserInfoURL string `json:"userInfoURL"` + Scopes []string `json:"scopes"` + GroupsKey string `json:"groupsKey"` + RootCAs []string `json:"rootCAs"` + InsecureSkipVerify bool `json:"insecureSkipVerify"` +} + +func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { + var err error + + oauthConn := &oauthConnector{ + clientID: c.ClientID, + clientSecret: c.ClientSecret, + tokenURL: c.TokenURL, + authorizationURL: c.AuthorizationURL, + userInfoURL: c.UserInfoURL, + scopes: c.Scopes, + groupsKey: c.GroupsKey, + redirectURI: c.RedirectURI, + logger: logger, + } + + oauthConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) + if err != nil { + return nil, err + } + + return oauthConn, err +} + +func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) { + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} + for _, rootCA := range rootCAs { + rootCABytes, err := ioutil.ReadFile(rootCA) + if err != nil { + return nil, fmt.Errorf("failed to read root-ca: %v", err) + } + if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { + return nil, fmt.Errorf("no certs found in root CA file %q", rootCA) + } + } + + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, nil +} + +func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { + + if c.redirectURI != callbackURL { + return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) + } + + oauth2Config := &oauth2.Config{ + ClientID: c.clientID, + ClientSecret: c.clientSecret, + Endpoint: oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL}, + RedirectURL: c.redirectURI, + Scopes: c.scopes, + } + + return oauth2Config.AuthCodeURL(state), nil +} + +func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { + + q := r.URL.Query() + if errType := q.Get("error"); errType != "" { + return identity, errors.New(q.Get("error_description")) + } + + oauth2Config := &oauth2.Config{ + ClientID: c.clientID, + ClientSecret: c.clientSecret, + Endpoint: oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL}, + RedirectURL: c.redirectURI, + Scopes: c.scopes, + } + + ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient) + + token, err := oauth2Config.Exchange(ctx, q.Get("code")) + if err != nil { + return identity, fmt.Errorf("OAuth connector: failed to get token: %v", err) + } + + client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)) + + userInfoResp, err := client.Get(c.userInfoURL) + if err != nil { + return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: %v", err) + } + + if userInfoResp.StatusCode != http.StatusOK { + return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: status %d", userInfoResp.StatusCode) + } + + defer userInfoResp.Body.Close() + + var userInfoResult map[string]interface{} + err = json.NewDecoder(userInfoResp.Body).Decode(&userInfoResult) + + if err != nil { + return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err) + } + + identity.UserID, _ = userInfoResult["user_id"].(string) + identity.Name, _ = userInfoResult["name"].(string) + identity.Username, _ = userInfoResult["user_name"].(string) + identity.Email, _ = userInfoResult["email"].(string) + identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) + + if s.Groups { + if c.groupsKey == "" { + c.groupsKey = "groups" + } + + groups := map[string]bool{} + + c.addGroupsFromMap(groups, userInfoResult) + c.addGroupsFromToken(groups, token.AccessToken) + + for groupName, _ := range groups { + identity.Groups = append(identity.Groups, groupName) + } + } + + if s.OfflineAccess { + data := connectorData{AccessToken: token.AccessToken} + connData, err := json.Marshal(data) + if err != nil { + return identity, fmt.Errorf("OAuth Connector: failed to parse connector data for offline access: %v", err) + } + identity.ConnectorData = connData + } + + return identity, nil +} + +func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[string]interface{}) error { + groupsClaim, ok := result[c.groupsKey].([]interface{}) + if !ok { + return errors.New("Cant convert to array") + } + + for _, group := range groupsClaim { + if groupString, ok := group.(string); ok { + groups[groupString] = true + } + } + + return nil +} + +func (c *oauthConnector) addGroupsFromToken(groups map[string]bool, token string) error { + parts := strings.Split(token, ".") + if len(parts) < 2 { + return errors.New("Invalid token") + } + + decoded, err := decode(parts[1]) + if err != nil { + return err + } + + var claimsMap map[string]interface{} + err = json.Unmarshal(decoded, &claimsMap) + if err != nil { + return err + } + + return c.addGroupsFromMap(groups, claimsMap) +} + +func decode(seg string) ([]byte, error) { + if l := len(seg) % 4; l > 0 { + seg += strings.Repeat("=", 4-l) + } + + return base64.URLEncoding.DecodeString(seg) +} diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go new file mode 100644 index 0000000000..2a43d72def --- /dev/null +++ b/connector/oauth/oauth_test.go @@ -0,0 +1,234 @@ +package oauth + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "sort" + "testing" + + "github.com/dexidp/dex/connector" + "github.com/sirupsen/logrus" + jose "gopkg.in/square/go-jose.v2" +) + +func TestOpen(t *testing.T) { + tokenClaims := map[string]interface{}{} + userInfoClaims := map[string]interface{}{} + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + + sort.Strings(conn.scopes) + + expectEqual(t, conn.clientID, "testClient") + expectEqual(t, conn.clientSecret, "testSecret") + expectEqual(t, conn.redirectURI, testServer.URL+"/callback") + expectEqual(t, conn.tokenURL, testServer.URL+"/token") + expectEqual(t, conn.authorizationURL, testServer.URL+"/authorize") + expectEqual(t, conn.userInfoURL, testServer.URL+"/userinfo") + expectEqual(t, len(conn.scopes), 2) + expectEqual(t, conn.scopes[0], "groups") + expectEqual(t, conn.scopes[1], "openid") +} + +func TestLoginURL(t *testing.T) { + tokenClaims := map[string]interface{}{} + userInfoClaims := map[string]interface{}{} + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + + loginURL, err := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") + expectEqual(t, err, nil) + + expectedURL, err := url.Parse(testServer.URL + "/authorize") + expectEqual(t, err, nil) + + values := url.Values{} + values.Add("client_id", "testClient") + values.Add("redirect_uri", conn.redirectURI) + values.Add("response_type", "code") + values.Add("scope", "openid groups") + values.Add("state", "some-state") + expectedURL.RawQuery = values.Encode() + + expectEqual(t, loginURL, expectedURL.String()) +} + +func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { + + tokenClaims := map[string]interface{}{} + + userInfoClaims := map[string]interface{}{ + "name": "test-name", + "user_name": "test-username", + "user_id": "test-user-id", + "email": "test-email", + "email_verified": true, + "groups_key": []string{"admin-group", "user-group"}, + } + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + req := newRequestWithAuthCode(t, testServer.URL, "some-code") + + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + expectEqual(t, err, nil) + + sort.Strings(identity.Groups) + expectEqual(t, len(identity.Groups), 2) + expectEqual(t, identity.Groups[0], "admin-group") + expectEqual(t, identity.Groups[1], "user-group") + expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.Username, "test-username") + expectEqual(t, identity.Email, "test-email") + expectEqual(t, identity.EmailVerified, true) +} + +func TestHandleCallBackForGroupsInToken(t *testing.T) { + + tokenClaims := map[string]interface{}{ + "groups_key": []string{"test-group"}, + } + + userInfoClaims := map[string]interface{}{ + "name": "test-name", + "user_name": "test-username", + "user_id": "test-user-id", + "email": "test-email", + "email_verified": true, + } + + testServer := testSetup(t, tokenClaims, userInfoClaims) + defer testServer.Close() + + conn := newConnector(t, testServer.URL) + req := newRequestWithAuthCode(t, testServer.URL, "some-code") + + identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + expectEqual(t, err, nil) + + expectEqual(t, len(identity.Groups), 1) + expectEqual(t, identity.Groups[0], "test-group") + expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.Username, "test-username") + expectEqual(t, identity.Email, "test-email") + expectEqual(t, identity.EmailVerified, true) +} + +func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { + + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + t.Fatal("Failed to generate rsa key", err) + } + + jwk := jose.JSONWebKey{ + Key: key, + KeyID: "some-key", + Algorithm: "RSA", + } + + mux := http.NewServeMux() + + mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { + token, err := newToken(&jwk, tokenClaims) + if err != nil { + t.Fatal("unable to generate token", err) + } + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(&map[string]string{ + "access_token": token, + "id_token": token, + "token_type": "Bearer", + }) + }) + + mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(userInfoClaims) + }) + + return httptest.NewServer(mux) +} + +func newToken(key *jose.JSONWebKey, claims map[string]interface{}) (string, error) { + signingKey := jose.SigningKey{Key: key, Algorithm: jose.RS256} + + signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{}) + if err != nil { + return "", fmt.Errorf("new signer: %v", err) + } + + payload, err := json.Marshal(claims) + if err != nil { + return "", fmt.Errorf("marshaling claims: %v", err) + } + + signature, err := signer.Sign(payload) + if err != nil { + return "", fmt.Errorf("signing payload: %v", err) + } + + return signature.CompactSerialize() +} + +func newConnector(t *testing.T, serverURL string) *oauthConnector { + testConfig := Config{ + ClientID: "testClient", + ClientSecret: "testSecret", + RedirectURI: serverURL + "/callback", + TokenURL: serverURL + "/token", + AuthorizationURL: serverURL + "/authorize", + UserInfoURL: serverURL + "/userinfo", + Scopes: []string{"openid", "groups"}, + GroupsKey: "groups_key", + } + + log := logrus.New() + + conn, err := testConfig.Open("id", log) + if err != nil { + t.Fatal(err) + } + + oauthConn, ok := conn.(*oauthConnector) + if !ok { + t.Fatal(errors.New("failed to convert to oauthConnector")) + } + + return oauthConn +} + +func newRequestWithAuthCode(t *testing.T, serverURL string, code string) *http.Request { + req, err := http.NewRequest("GET", serverURL, nil) + if err != nil { + t.Fatal("failed to create request", err) + } + + values := req.URL.Query() + values.Add("code", code) + req.URL.RawQuery = values.Encode() + + return req +} + +func expectEqual(t *testing.T, a interface{}, b interface{}) { + if !reflect.DeepEqual(a, b) { + t.Fatalf("Expected %+v to equal %+v", a, b) + } +} diff --git a/server/server.go b/server/server.go index a79b7cfd3b..4a42ec341d 100644 --- a/server/server.go +++ b/server/server.go @@ -35,6 +35,7 @@ import ( "github.com/dexidp/dex/connector/linkedin" "github.com/dexidp/dex/connector/microsoft" "github.com/dexidp/dex/connector/mock" + "github.com/dexidp/dex/connector/oauth" "github.com/dexidp/dex/connector/oidc" "github.com/dexidp/dex/connector/openshift" "github.com/dexidp/dex/connector/saml" @@ -501,6 +502,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{ "gitlab": func() ConnectorConfig { return new(gitlab.Config) }, "google": func() ConnectorConfig { return new(google.Config) }, "oidc": func() ConnectorConfig { return new(oidc.Config) }, + "oauth": func() ConnectorConfig { return new(oauth.Config) }, "saml": func() ConnectorConfig { return new(saml.Config) }, "authproxy": func() ConnectorConfig { return new(authproxy.Config) }, "linkedin": func() ConnectorConfig { return new(linkedin.Config) }, From a6be30b7ceee1a6e5b7c2b786fb4f9c1b4702ba8 Mon Sep 17 00:00:00 2001 From: Josh Winters Date: Wed, 27 Feb 2019 14:59:44 -0500 Subject: [PATCH 14/30] Make oauth user name and user id configurable Signed-off-by: Josh Winters Co-authored-by: Mark Huang --- connector/oauth/oauth.go | 26 ++++++++++++++++++++------ connector/oauth/oauth_test.go | 12 ++++++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 7bf480cdc5..74424c2db4 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -28,6 +28,8 @@ type oauthConnector struct { userInfoURL string scopes []string groupsKey string + userIDKey string + userNameKey string httpClient *http.Client logger log.Logger } @@ -45,6 +47,8 @@ type Config struct { UserInfoURL string `json:"userInfoURL"` Scopes []string `json:"scopes"` GroupsKey string `json:"groupsKey"` + UserIDKey string `json:"userIDKey"` + UserNameKey string `json:"userNameKey"` RootCAs []string `json:"rootCAs"` InsecureSkipVerify bool `json:"insecureSkipVerify"` } @@ -60,6 +64,8 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) userInfoURL: c.UserInfoURL, scopes: c.Scopes, groupsKey: c.GroupsKey, + userIDKey: c.UserIDKey, + userNameKey: c.UserNameKey, redirectURI: c.RedirectURI, logger: logger, } @@ -165,17 +171,25 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err) } - identity.UserID, _ = userInfoResult["user_id"].(string) + if c.userIDKey == "" { + c.userIDKey = "user_id" + } + + if c.userNameKey == "" { + c.userNameKey = "user_name" + } + + if c.groupsKey == "" { + c.groupsKey = "groups" + } + + identity.UserID, _ = userInfoResult[c.userIDKey].(string) + identity.Username, _ = userInfoResult[c.userNameKey].(string) identity.Name, _ = userInfoResult["name"].(string) - identity.Username, _ = userInfoResult["user_name"].(string) identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) if s.Groups { - if c.groupsKey == "" { - c.groupsKey = "groups" - } - groups := map[string]bool{} c.addGroupsFromMap(groups, userInfoResult) diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index 2a43d72def..cd39d7f9f4 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -72,8 +72,8 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { userInfoClaims := map[string]interface{}{ "name": "test-name", - "user_name": "test-username", - "user_id": "test-user-id", + "user_id_key": "test-user-id", + "user_name_key": "test-username", "email": "test-email", "email_verified": true, "groups_key": []string{"admin-group", "user-group"}, @@ -93,6 +93,7 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { expectEqual(t, identity.Groups[0], "admin-group") expectEqual(t, identity.Groups[1], "user-group") expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") expectEqual(t, identity.EmailVerified, true) @@ -106,8 +107,8 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { userInfoClaims := map[string]interface{}{ "name": "test-name", - "user_name": "test-username", - "user_id": "test-user-id", + "user_id_key": "test-user-id", + "user_name_key": "test-username", "email": "test-email", "email_verified": true, } @@ -124,6 +125,7 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { expectEqual(t, len(identity.Groups), 1) expectEqual(t, identity.Groups[0], "test-group") expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") expectEqual(t, identity.EmailVerified, true) @@ -197,6 +199,8 @@ func newConnector(t *testing.T, serverURL string) *oauthConnector { UserInfoURL: serverURL + "/userinfo", Scopes: []string{"openid", "groups"}, GroupsKey: "groups_key", + UserIDKey: "user_id_key", + UserNameKey: "user_name_key", } log := logrus.New() From d2c3d4806d1f8b849258139af698b74895f82505 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Thu, 16 Jan 2020 15:30:47 -0500 Subject: [PATCH 15/30] use PreferredUsername Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 13 ++++++------- connector/oauth/oauth_test.go | 10 ++++------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 74424c2db4..056a1715b6 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -14,9 +14,10 @@ import ( "strings" "time" + "golang.org/x/oauth2" + "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/log" - "golang.org/x/oauth2" ) type oauthConnector struct { @@ -113,7 +114,6 @@ func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, err } func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { - if c.redirectURI != callbackURL { return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI) } @@ -130,7 +130,6 @@ func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state st } func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { - q := r.URL.Query() if errType := q.Get("error"); errType != "" { return identity, errors.New(q.Get("error_description")) @@ -185,7 +184,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id identity.UserID, _ = userInfoResult[c.userIDKey].(string) identity.Username, _ = userInfoResult[c.userNameKey].(string) - identity.Name, _ = userInfoResult["name"].(string) + identity.PreferredUsername, _ = userInfoResult["name"].(string) identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) @@ -195,7 +194,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id c.addGroupsFromMap(groups, userInfoResult) c.addGroupsFromToken(groups, token.AccessToken) - for groupName, _ := range groups { + for groupName := range groups { identity.Groups = append(identity.Groups, groupName) } } @@ -215,7 +214,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[string]interface{}) error { groupsClaim, ok := result[c.groupsKey].([]interface{}) if !ok { - return errors.New("Cant convert to array") + return errors.New("cant convert to array") } for _, group := range groupsClaim { @@ -230,7 +229,7 @@ func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[str func (c *oauthConnector) addGroupsFromToken(groups map[string]bool, token string) error { parts := strings.Split(token, ".") if len(parts) < 2 { - return errors.New("Invalid token") + return errors.New("invalid token") } decoded, err := decode(parts[1]) diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index cd39d7f9f4..a496bb8230 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -13,9 +13,10 @@ import ( "sort" "testing" - "github.com/dexidp/dex/connector" "github.com/sirupsen/logrus" jose "gopkg.in/square/go-jose.v2" + + "github.com/dexidp/dex/connector" ) func TestOpen(t *testing.T) { @@ -67,7 +68,6 @@ func TestLoginURL(t *testing.T) { } func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { - tokenClaims := map[string]interface{}{} userInfoClaims := map[string]interface{}{ @@ -92,7 +92,7 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { expectEqual(t, len(identity.Groups), 2) expectEqual(t, identity.Groups[0], "admin-group") expectEqual(t, identity.Groups[1], "user-group") - expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.PreferredUsername, "test-name") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") @@ -100,7 +100,6 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { } func TestHandleCallBackForGroupsInToken(t *testing.T) { - tokenClaims := map[string]interface{}{ "groups_key": []string{"test-group"}, } @@ -124,7 +123,7 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { expectEqual(t, len(identity.Groups), 1) expectEqual(t, identity.Groups[0], "test-group") - expectEqual(t, identity.Name, "test-name") + expectEqual(t, identity.PreferredUsername, "test-name") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") @@ -132,7 +131,6 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { } func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { - key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { t.Fatal("Failed to generate rsa key", err) From 2b5b19c2776aeca47c1700ee66a68f2c5fd23e82 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Fri, 31 Jul 2020 14:25:28 -0400 Subject: [PATCH 16/30] add docs for oauth connector Signed-off-by: Rui Yang --- README.md | 1 + docs/connectors/oauth.md | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 docs/connectors/oauth.md diff --git a/README.md b/README.md index 854a0ce9f6..4069edfe3b 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ Dex implements the following connectors: | [OpenShift](https://dexidp.io/docs/connectors/openshift/) | no | yes | no | alpha | | | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | | +| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | beta | Stable, beta, and alpha are defined as: diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md new file mode 100644 index 0000000000..b4a5e9a569 --- /dev/null +++ b/docs/connectors/oauth.md @@ -0,0 +1,49 @@ +# Authentication using Generic OAuth 2.0 provider + +## Overview + +Dex users can make use of this connector to work with standards-compliant [OAuth 2.0](https://oauth.net/2/) authorization provider, in case of that authorization provider is not in the Dex connectors list. + +## Configuration + +The following is an example of a configuration for using OAuth connector with Reddit. + +```yaml +connectors: +- type: oauth + # ID of OAuth 2.0 provider + id: reddit + # Name of OAuth 2.0 provider + name: reddit + config: + # Connector config values starting with a "$" will read from the environment. + clientID: $REDDIT_CLIENT_ID + clientSecret: $REDDIT_CLIENT_SECRET + redirectURI: http://127.0.0.1:5556/callback + + tokenURL: https://www.reddit.com/api/v1/access_token + authorizationURL: https://www.reddit.com/api/v1/authorize + userInfoURL: https: https://www.reddit.com/api/v1/me + + # Optional: Specify whether to communicate to Auth provider without validating SSL certificates + # insecureSkipVerify: false + + # Optional: The location of file containing SSL certificates to commmunicate to Auth provider + # rootCAs: /etc/ssl/reddit.pem + + # Optional: List of scopes to request Auth provider for access user account + # scopes: + # - identity + + # Optional: Configurable keys for user id field look up + # Default: groups + # groupsKey: + + # Optional: Configurable keys for name field look up + # Default: user_id + # userIDKey: + + # Optional: Configurable keys for username field look up + # Default: user_name + # userNameKey: +``` \ No newline at end of file From bee35343833aa89e99946e96af9063169866aebb Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Tue, 4 Aug 2020 11:00:40 -0400 Subject: [PATCH 17/30] add configurable preferred_username key Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 56 +++++++++++++++++++---------------- connector/oauth/oauth_test.go | 28 ++++++++++-------- docs/connectors/oauth.md | 12 ++++---- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 056a1715b6..849db33120 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -21,18 +21,19 @@ import ( ) type oauthConnector struct { - clientID string - clientSecret string - redirectURI string - tokenURL string - authorizationURL string - userInfoURL string - scopes []string - groupsKey string - userIDKey string - userNameKey string - httpClient *http.Client - logger log.Logger + clientID string + clientSecret string + redirectURI string + tokenURL string + authorizationURL string + userInfoURL string + scopes []string + groupsKey string + userIDKey string + userNameKey string + preferredUsernameKey string + httpClient *http.Client + logger log.Logger } type connectorData struct { @@ -40,18 +41,19 @@ type connectorData struct { } type Config struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - TokenURL string `json:"tokenURL"` - AuthorizationURL string `json:"authorizationURL"` - UserInfoURL string `json:"userInfoURL"` - Scopes []string `json:"scopes"` - GroupsKey string `json:"groupsKey"` - UserIDKey string `json:"userIDKey"` - UserNameKey string `json:"userNameKey"` - RootCAs []string `json:"rootCAs"` - InsecureSkipVerify bool `json:"insecureSkipVerify"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + TokenURL string `json:"tokenURL"` + AuthorizationURL string `json:"authorizationURL"` + UserInfoURL string `json:"userInfoURL"` + Scopes []string `json:"scopes"` + GroupsKey string `json:"groupsKey"` + UserIDKey string `json:"userIDKey"` + UserNameKey string `json:"userNameKey"` + PreferredUsernameKey string `json:"preferredUsernameKey"` + RootCAs []string `json:"rootCAs"` + InsecureSkipVerify bool `json:"insecureSkipVerify"` } func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { @@ -182,9 +184,13 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id c.groupsKey = "groups" } + if c.preferredUsernameKey == "" { + c.preferredUsernameKey = "preferred_username" + } + identity.UserID, _ = userInfoResult[c.userIDKey].(string) identity.Username, _ = userInfoResult[c.userNameKey].(string) - identity.PreferredUsername, _ = userInfoResult["name"].(string) + identity.PreferredUsername, _ = userInfoResult[c.preferredUsernameKey].(string) identity.Email, _ = userInfoResult["email"].(string) identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index a496bb8230..3ee06207f2 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -71,12 +71,13 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { tokenClaims := map[string]interface{}{} userInfoClaims := map[string]interface{}{ - "name": "test-name", - "user_id_key": "test-user-id", - "user_name_key": "test-username", - "email": "test-email", - "email_verified": true, - "groups_key": []string{"admin-group", "user-group"}, + "name": "test-name", + "user_id_key": "test-user-id", + "user_name_key": "test-username", + "preferred_username": "test-preferred-username", + "email": "test-email", + "email_verified": true, + "groups_key": []string{"admin-group", "user-group"}, } testServer := testSetup(t, tokenClaims, userInfoClaims) @@ -92,9 +93,9 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { expectEqual(t, len(identity.Groups), 2) expectEqual(t, identity.Groups[0], "admin-group") expectEqual(t, identity.Groups[1], "user-group") - expectEqual(t, identity.PreferredUsername, "test-name") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") + expectEqual(t, identity.PreferredUsername, "test-preferred-username") expectEqual(t, identity.Email, "test-email") expectEqual(t, identity.EmailVerified, true) } @@ -105,11 +106,12 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { } userInfoClaims := map[string]interface{}{ - "name": "test-name", - "user_id_key": "test-user-id", - "user_name_key": "test-username", - "email": "test-email", - "email_verified": true, + "name": "test-name", + "user_id_key": "test-user-id", + "user_name_key": "test-username", + "preferred_username": "test-preferred-username", + "email": "test-email", + "email_verified": true, } testServer := testSetup(t, tokenClaims, userInfoClaims) @@ -123,7 +125,7 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { expectEqual(t, len(identity.Groups), 1) expectEqual(t, identity.Groups[0], "test-group") - expectEqual(t, identity.PreferredUsername, "test-name") + expectEqual(t, identity.PreferredUsername, "test-preferred-username") expectEqual(t, identity.UserID, "test-user-id") expectEqual(t, identity.Username, "test-username") expectEqual(t, identity.Email, "test-email") diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md index b4a5e9a569..2092b49518 100644 --- a/docs/connectors/oauth.md +++ b/docs/connectors/oauth.md @@ -35,15 +35,15 @@ connectors: # scopes: # - identity - # Optional: Configurable keys for user id field look up + # Optional: Configurable keys for groups claim look up # Default: groups # groupsKey: - # Optional: Configurable keys for name field look up + # Optional: Configurable keys for user ID claim look up # Default: user_id # userIDKey: - # Optional: Configurable keys for username field look up - # Default: user_name - # userNameKey: -``` \ No newline at end of file + # Optional: Configurable keys for preferred username claim look up + # Default: preferred_username + # preferredUsernameKey: +``` From 84b6d5f8d3ace3e14e97e73bf0f7ecbd2c3189bd Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Tue, 6 Oct 2020 21:04:06 -0400 Subject: [PATCH 18/30] use testify in oauth tests Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 2 +- connector/oauth/oauth_test.go | 68 ++++++++++++++++------------------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index 849db33120..d14319a51b 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -220,7 +220,7 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id func (c *oauthConnector) addGroupsFromMap(groups map[string]bool, result map[string]interface{}) error { groupsClaim, ok := result[c.groupsKey].([]interface{}) if !ok { - return errors.New("cant convert to array") + return errors.New("cannot convert to slice") } for _, group := range groupsClaim { diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index 3ee06207f2..7ba12be15c 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -9,11 +9,11 @@ import ( "net/http" "net/http/httptest" "net/url" - "reflect" "sort" "testing" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" jose "gopkg.in/square/go-jose.v2" "github.com/dexidp/dex/connector" @@ -30,15 +30,15 @@ func TestOpen(t *testing.T) { sort.Strings(conn.scopes) - expectEqual(t, conn.clientID, "testClient") - expectEqual(t, conn.clientSecret, "testSecret") - expectEqual(t, conn.redirectURI, testServer.URL+"/callback") - expectEqual(t, conn.tokenURL, testServer.URL+"/token") - expectEqual(t, conn.authorizationURL, testServer.URL+"/authorize") - expectEqual(t, conn.userInfoURL, testServer.URL+"/userinfo") - expectEqual(t, len(conn.scopes), 2) - expectEqual(t, conn.scopes[0], "groups") - expectEqual(t, conn.scopes[1], "openid") + assert.Equal(t, conn.clientID, "testClient") + assert.Equal(t, conn.clientSecret, "testSecret") + assert.Equal(t, conn.redirectURI, testServer.URL+"/callback") + assert.Equal(t, conn.tokenURL, testServer.URL+"/token") + assert.Equal(t, conn.authorizationURL, testServer.URL+"/authorize") + assert.Equal(t, conn.userInfoURL, testServer.URL+"/userinfo") + assert.Equal(t, len(conn.scopes), 2) + assert.Equal(t, conn.scopes[0], "groups") + assert.Equal(t, conn.scopes[1], "openid") } func TestLoginURL(t *testing.T) { @@ -51,10 +51,10 @@ func TestLoginURL(t *testing.T) { conn := newConnector(t, testServer.URL) loginURL, err := conn.LoginURL(connector.Scopes{}, conn.redirectURI, "some-state") - expectEqual(t, err, nil) + assert.Equal(t, err, nil) expectedURL, err := url.Parse(testServer.URL + "/authorize") - expectEqual(t, err, nil) + assert.Equal(t, err, nil) values := url.Values{} values.Add("client_id", "testClient") @@ -64,7 +64,7 @@ func TestLoginURL(t *testing.T) { values.Add("state", "some-state") expectedURL.RawQuery = values.Encode() - expectEqual(t, loginURL, expectedURL.String()) + assert.Equal(t, loginURL, expectedURL.String()) } func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { @@ -87,17 +87,17 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { req := newRequestWithAuthCode(t, testServer.URL, "some-code") identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) - expectEqual(t, err, nil) + assert.Equal(t, err, nil) sort.Strings(identity.Groups) - expectEqual(t, len(identity.Groups), 2) - expectEqual(t, identity.Groups[0], "admin-group") - expectEqual(t, identity.Groups[1], "user-group") - expectEqual(t, identity.UserID, "test-user-id") - expectEqual(t, identity.Username, "test-username") - expectEqual(t, identity.PreferredUsername, "test-preferred-username") - expectEqual(t, identity.Email, "test-email") - expectEqual(t, identity.EmailVerified, true) + assert.Equal(t, len(identity.Groups), 2) + assert.Equal(t, identity.Groups[0], "admin-group") + assert.Equal(t, identity.Groups[1], "user-group") + assert.Equal(t, identity.UserID, "test-user-id") + assert.Equal(t, identity.Username, "test-username") + assert.Equal(t, identity.PreferredUsername, "test-preferred-username") + assert.Equal(t, identity.Email, "test-email") + assert.Equal(t, identity.EmailVerified, true) } func TestHandleCallBackForGroupsInToken(t *testing.T) { @@ -121,15 +121,15 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { req := newRequestWithAuthCode(t, testServer.URL, "some-code") identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) - expectEqual(t, err, nil) - - expectEqual(t, len(identity.Groups), 1) - expectEqual(t, identity.Groups[0], "test-group") - expectEqual(t, identity.PreferredUsername, "test-preferred-username") - expectEqual(t, identity.UserID, "test-user-id") - expectEqual(t, identity.Username, "test-username") - expectEqual(t, identity.Email, "test-email") - expectEqual(t, identity.EmailVerified, true) + assert.Equal(t, err, nil) + + assert.Equal(t, len(identity.Groups), 1) + assert.Equal(t, identity.Groups[0], "test-group") + assert.Equal(t, identity.PreferredUsername, "test-preferred-username") + assert.Equal(t, identity.UserID, "test-user-id") + assert.Equal(t, identity.Username, "test-username") + assert.Equal(t, identity.Email, "test-email") + assert.Equal(t, identity.EmailVerified, true) } func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { @@ -230,9 +230,3 @@ func newRequestWithAuthCode(t *testing.T, serverURL string, code string) *http.R return req } - -func expectEqual(t *testing.T, a interface{}, b interface{}) { - if !reflect.DeepEqual(a, b) { - t.Fatalf("Expected %+v to equal %+v", a, b) - } -} From 889a323db5ef01d9eda76c29f226dbf2bfdb75a9 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 19 Oct 2020 22:18:25 -0400 Subject: [PATCH 19/30] use claim mappings when retrieving user identity Signed-off-by: Rui Yang --- connector/oauth/oauth.go | 84 ++++++++++++++++++++++------------- connector/oauth/oauth_test.go | 19 ++++---- docs/connectors/oauth.md | 33 ++++++++++---- 3 files changed, 88 insertions(+), 48 deletions(-) diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index d14319a51b..ad83dad226 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -28,10 +28,12 @@ type oauthConnector struct { authorizationURL string userInfoURL string scopes []string - groupsKey string userIDKey string userNameKey string preferredUsernameKey string + emailKey string + emailVerifiedKey string + groupsKey string httpClient *http.Client logger log.Logger } @@ -41,36 +43,43 @@ type connectorData struct { } type Config struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - RedirectURI string `json:"redirectURI"` - TokenURL string `json:"tokenURL"` - AuthorizationURL string `json:"authorizationURL"` - UserInfoURL string `json:"userInfoURL"` - Scopes []string `json:"scopes"` - GroupsKey string `json:"groupsKey"` - UserIDKey string `json:"userIDKey"` - UserNameKey string `json:"userNameKey"` - PreferredUsernameKey string `json:"preferredUsernameKey"` - RootCAs []string `json:"rootCAs"` - InsecureSkipVerify bool `json:"insecureSkipVerify"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + RedirectURI string `json:"redirectURI"` + TokenURL string `json:"tokenURL"` + AuthorizationURL string `json:"authorizationURL"` + UserInfoURL string `json:"userInfoURL"` + Scopes []string `json:"scopes"` + RootCAs []string `json:"rootCAs"` + InsecureSkipVerify bool `json:"insecureSkipVerify"` + UserIDKey string `json:"userIDKey"` // defaults to "id" + ClaimMapping struct { + UserNameKey string `json:"userNameKey"` // defaults to "user_name" + PreferredUsernameKey string `json:"preferredUsernameKey"` // defaults to "preferred_username" + GroupsKey string `json:"groupsKey"` // defaults to "groups" + EmailKey string `json:"emailKey"` // defaults to "email" + EmailVerifiedKey string `json:"emailVerifiedKey"` // defaults to "email_verified" + } `json:"claimMapping"` } func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error oauthConn := &oauthConnector{ - clientID: c.ClientID, - clientSecret: c.ClientSecret, - tokenURL: c.TokenURL, - authorizationURL: c.AuthorizationURL, - userInfoURL: c.UserInfoURL, - scopes: c.Scopes, - groupsKey: c.GroupsKey, - userIDKey: c.UserIDKey, - userNameKey: c.UserNameKey, - redirectURI: c.RedirectURI, - logger: logger, + clientID: c.ClientID, + clientSecret: c.ClientSecret, + tokenURL: c.TokenURL, + authorizationURL: c.AuthorizationURL, + userInfoURL: c.UserInfoURL, + scopes: c.Scopes, + redirectURI: c.RedirectURI, + logger: logger, + userIDKey: c.UserIDKey, + userNameKey: c.ClaimMapping.UserNameKey, + preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey, + groupsKey: c.ClaimMapping.GroupsKey, + emailKey: c.ClaimMapping.EmailKey, + emailVerifiedKey: c.ClaimMapping.EmailVerifiedKey, } oauthConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) @@ -173,26 +182,39 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id } if c.userIDKey == "" { - c.userIDKey = "user_id" + c.userIDKey = "id" + } + + userID, found := userInfoResult[c.userIDKey].(string) + if !found { + return identity, fmt.Errorf("OAuth Connector: not found %v claim", c.userIDKey) } + identity.UserID = userID if c.userNameKey == "" { c.userNameKey = "user_name" } + if c.preferredUsernameKey == "" { + c.preferredUsernameKey = "preferred_username" + } + if c.groupsKey == "" { c.groupsKey = "groups" } - if c.preferredUsernameKey == "" { - c.preferredUsernameKey = "preferred_username" + if c.emailKey == "" { + c.emailKey = "email" + } + + if c.emailVerifiedKey == "" { + c.emailVerifiedKey = "email_verified" } - identity.UserID, _ = userInfoResult[c.userIDKey].(string) identity.Username, _ = userInfoResult[c.userNameKey].(string) identity.PreferredUsername, _ = userInfoResult[c.preferredUsernameKey].(string) - identity.Email, _ = userInfoResult["email"].(string) - identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) + identity.Email, _ = userInfoResult[c.emailKey].(string) + identity.EmailVerified, _ = userInfoResult[c.emailVerifiedKey].(bool) if s.Groups { groups := map[string]bool{} diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index 7ba12be15c..b8074aa452 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -75,8 +75,8 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { "user_id_key": "test-user-id", "user_name_key": "test-username", "preferred_username": "test-preferred-username", - "email": "test-email", - "email_verified": true, + "mail": "mod_mail", + "has_verified_email": false, "groups_key": []string{"admin-group", "user-group"}, } @@ -96,8 +96,8 @@ func TestHandleCallBackForGroupsInUserInfo(t *testing.T) { assert.Equal(t, identity.UserID, "test-user-id") assert.Equal(t, identity.Username, "test-username") assert.Equal(t, identity.PreferredUsername, "test-preferred-username") - assert.Equal(t, identity.Email, "test-email") - assert.Equal(t, identity.EmailVerified, true) + assert.Equal(t, identity.Email, "mod_mail") + assert.Equal(t, identity.EmailVerified, false) } func TestHandleCallBackForGroupsInToken(t *testing.T) { @@ -128,8 +128,8 @@ func TestHandleCallBackForGroupsInToken(t *testing.T) { assert.Equal(t, identity.PreferredUsername, "test-preferred-username") assert.Equal(t, identity.UserID, "test-user-id") assert.Equal(t, identity.Username, "test-username") - assert.Equal(t, identity.Email, "test-email") - assert.Equal(t, identity.EmailVerified, true) + assert.Equal(t, identity.Email, "") + assert.Equal(t, identity.EmailVerified, false) } func testSetup(t *testing.T, tokenClaims map[string]interface{}, userInfoClaims map[string]interface{}) *httptest.Server { @@ -198,11 +198,14 @@ func newConnector(t *testing.T, serverURL string) *oauthConnector { AuthorizationURL: serverURL + "/authorize", UserInfoURL: serverURL + "/userinfo", Scopes: []string{"openid", "groups"}, - GroupsKey: "groups_key", UserIDKey: "user_id_key", - UserNameKey: "user_name_key", } + testConfig.ClaimMapping.UserNameKey = "user_name_key" + testConfig.ClaimMapping.GroupsKey = "groups_key" + testConfig.ClaimMapping.EmailKey = "mail" + testConfig.ClaimMapping.EmailVerifiedKey = "has_verified_email" + log := logrus.New() conn, err := testConfig.Open("id", log) diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md index 2092b49518..d129bc5fbb 100644 --- a/docs/connectors/oauth.md +++ b/docs/connectors/oauth.md @@ -35,15 +35,30 @@ connectors: # scopes: # - identity - # Optional: Configurable keys for groups claim look up - # Default: groups - # groupsKey: - - # Optional: Configurable keys for user ID claim look up - # Default: user_id + # Optional: Configurable keys for user ID look up + # Default: id # userIDKey: - # Optional: Configurable keys for preferred username claim look up - # Default: preferred_username - # preferredUsernameKey: + # Auth roviders return non-standard user identity profile + # Use claimMapping to map those user infomations to standard claims: + claimMapping: + # Optional: Configurable keys for user name look up + # Default: user_name + # userNameKey: + + # Optional: Configurable keys for preferred username look up + # Default: preferred_username + # preferredUsernameKey: + + # Optional: Configurable keys for user groups look up + # Default: groups + # groupsKey: + + # Optional: Configurable keys for email look up + # Default: email + # emailKey: + + # Optional: Configurable keys for email verified look up + # Default: email_verified + # emailVerifiedKey: ``` From 1f329bfa305bc5fd9ecd2b7e82c985c047baec8b Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Wed, 4 Nov 2020 21:59:10 -0500 Subject: [PATCH 20/30] readme minor fix for oauth connector Signed-off-by: Rui Yang --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4069edfe3b..e60391bebe 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Dex implements the following connectors: | [SAML 2.0](https://dexidp.io/docs/connectors/saml/) | no | yes | no | stable | WARNING: Unmaintained and likely vulnerable to auth bypasses ([#1884](https://github.com/dexidp/dex/discussions/1884)) | | [GitLab](https://dexidp.io/docs/connectors/gitlab/) | yes | yes | yes | beta | | | [OpenID Connect](https://dexidp.io/docs/connectors/oidc/) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. | +| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth.md) | no | yes | yes | beta | | | [Google](https://dexidp.io/docs/connectors/google/) | yes | yes | yes | alpha | | | [LinkedIn](https://dexidp.io/docs/connectors/linkedin/) | yes | no | no | beta | | | [Microsoft](https://dexidp.io/docs/connectors/microsoft/) | yes | yes | no | beta | | @@ -80,7 +81,7 @@ Dex implements the following connectors: | [OpenShift](https://dexidp.io/docs/connectors/openshift/) | no | yes | no | alpha | | | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | | -| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | beta | +| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | Stable, beta, and alpha are defined as: From ec95501ad962535674686739278ad2931fdef7f6 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 15 Mar 2021 14:19:49 -0400 Subject: [PATCH 21/30] move oauth connector doc to dex website repo move default key values configure to connector construct function Signed-off-by: Rui Yang --- README.md | 3 +- connector/oauth/oauth.go | 48 +++++++++++++++--------------- docs/connectors/oauth.md | 64 ---------------------------------------- 3 files changed, 25 insertions(+), 90 deletions(-) delete mode 100644 docs/connectors/oauth.md diff --git a/README.md b/README.md index e60391bebe..d75b159b97 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Dex implements the following connectors: | [SAML 2.0](https://dexidp.io/docs/connectors/saml/) | no | yes | no | stable | WARNING: Unmaintained and likely vulnerable to auth bypasses ([#1884](https://github.com/dexidp/dex/discussions/1884)) | | [GitLab](https://dexidp.io/docs/connectors/gitlab/) | yes | yes | yes | beta | | | [OpenID Connect](https://dexidp.io/docs/connectors/oidc/) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. | -| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth.md) | no | yes | yes | beta | | +| [OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | | | [Google](https://dexidp.io/docs/connectors/google/) | yes | yes | yes | alpha | | | [LinkedIn](https://dexidp.io/docs/connectors/linkedin/) | yes | no | no | beta | | | [Microsoft](https://dexidp.io/docs/connectors/microsoft/) | yes | yes | no | beta | | @@ -81,7 +81,6 @@ Dex implements the following connectors: | [OpenShift](https://dexidp.io/docs/connectors/openshift/) | no | yes | no | alpha | | | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config | | [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | | -| [Generic OAuth 2.0](https://dexidp.io/docs/connectors/oauth/) | no | yes | yes | alpha | Stable, beta, and alpha are defined as: diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index ad83dad226..c709531e2a 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -65,6 +65,30 @@ type Config struct { func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error + if c.UserIDKey == "" { + c.UserIDKey = "id" + } + + if c.ClaimMapping.UserNameKey == "" { + c.ClaimMapping.UserNameKey = "user_name" + } + + if c.ClaimMapping.PreferredUsernameKey == "" { + c.ClaimMapping.PreferredUsernameKey = "preferred_username" + } + + if c.ClaimMapping.GroupsKey == "" { + c.ClaimMapping.GroupsKey = "groups" + } + + if c.ClaimMapping.EmailKey == "" { + c.ClaimMapping.EmailKey = "email" + } + + if c.ClaimMapping.EmailVerifiedKey == "" { + c.ClaimMapping.EmailVerifiedKey = "email_verified" + } + oauthConn := &oauthConnector{ clientID: c.ClientID, clientSecret: c.ClientSecret, @@ -181,36 +205,12 @@ func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (id return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err) } - if c.userIDKey == "" { - c.userIDKey = "id" - } - userID, found := userInfoResult[c.userIDKey].(string) if !found { return identity, fmt.Errorf("OAuth Connector: not found %v claim", c.userIDKey) } identity.UserID = userID - if c.userNameKey == "" { - c.userNameKey = "user_name" - } - - if c.preferredUsernameKey == "" { - c.preferredUsernameKey = "preferred_username" - } - - if c.groupsKey == "" { - c.groupsKey = "groups" - } - - if c.emailKey == "" { - c.emailKey = "email" - } - - if c.emailVerifiedKey == "" { - c.emailVerifiedKey = "email_verified" - } - identity.Username, _ = userInfoResult[c.userNameKey].(string) identity.PreferredUsername, _ = userInfoResult[c.preferredUsernameKey].(string) identity.Email, _ = userInfoResult[c.emailKey].(string) diff --git a/docs/connectors/oauth.md b/docs/connectors/oauth.md deleted file mode 100644 index d129bc5fbb..0000000000 --- a/docs/connectors/oauth.md +++ /dev/null @@ -1,64 +0,0 @@ -# Authentication using Generic OAuth 2.0 provider - -## Overview - -Dex users can make use of this connector to work with standards-compliant [OAuth 2.0](https://oauth.net/2/) authorization provider, in case of that authorization provider is not in the Dex connectors list. - -## Configuration - -The following is an example of a configuration for using OAuth connector with Reddit. - -```yaml -connectors: -- type: oauth - # ID of OAuth 2.0 provider - id: reddit - # Name of OAuth 2.0 provider - name: reddit - config: - # Connector config values starting with a "$" will read from the environment. - clientID: $REDDIT_CLIENT_ID - clientSecret: $REDDIT_CLIENT_SECRET - redirectURI: http://127.0.0.1:5556/callback - - tokenURL: https://www.reddit.com/api/v1/access_token - authorizationURL: https://www.reddit.com/api/v1/authorize - userInfoURL: https: https://www.reddit.com/api/v1/me - - # Optional: Specify whether to communicate to Auth provider without validating SSL certificates - # insecureSkipVerify: false - - # Optional: The location of file containing SSL certificates to commmunicate to Auth provider - # rootCAs: /etc/ssl/reddit.pem - - # Optional: List of scopes to request Auth provider for access user account - # scopes: - # - identity - - # Optional: Configurable keys for user ID look up - # Default: id - # userIDKey: - - # Auth roviders return non-standard user identity profile - # Use claimMapping to map those user infomations to standard claims: - claimMapping: - # Optional: Configurable keys for user name look up - # Default: user_name - # userNameKey: - - # Optional: Configurable keys for preferred username look up - # Default: preferred_username - # preferredUsernameKey: - - # Optional: Configurable keys for user groups look up - # Default: groups - # groupsKey: - - # Optional: Configurable keys for email look up - # Default: email - # emailKey: - - # Optional: Configurable keys for email verified look up - # Default: email_verified - # emailVerifiedKey: -``` From 1eeff38095bd419e02bde5524947bcf927d8604f Mon Sep 17 00:00:00 2001 From: Josh Winters Date: Wed, 11 Dec 2019 11:03:58 -0500 Subject: [PATCH 22/30] Add support for client_credentials grant type Co-authored-by: Rui Yang Signed-off-by: Josh Winters --- server/handlers.go | 25 +++++++++++++++++++++++++ server/oauth2.go | 1 + 2 files changed, 26 insertions(+) diff --git a/server/handlers.go b/server/handlers.go index eb65f490bd..0dc9900ef1 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -697,6 +697,8 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { s.handleRefreshToken(w, r, client) case grantTypePassword: s.handlePasswordGrant(w, r, client) + case grantTypeClientCredentials: + s.handleClientCredentialsGrant(w, r, client) default: s.tokenErrHelper(w, errInvalidGrant, "", http.StatusBadRequest) } @@ -1147,6 +1149,29 @@ func (s *Server) handleUserInfo(w http.ResponseWriter, r *http.Request) { w.Write(claims) } +func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Request, client storage.Client) { + if err := r.ParseForm(); err != nil { + s.tokenErrHelper(w, errInvalidRequest, "Couldn't parse data", http.StatusBadRequest) + return + } + q := r.Form + + nonce := q.Get("nonce") + scopes := strings.Fields(q.Get("scope")) + + claims := storage.Claims{UserID: client.ID} + + accessToken := storage.NewID() + idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, nonce, accessToken, "", "client") + if err != nil { + s.tokenErrHelper(w, errServerError, fmt.Sprintf("failed to create ID token: %v", err), http.StatusInternalServerError) + return + } + + resp := s.toAccessTokenResponse(idToken, accessToken, "", expiry) + s.writeAccessToken(w, resp) +} + func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, client storage.Client) { // Parse the fields if err := r.ParseForm(); err != nil { diff --git a/server/oauth2.go b/server/oauth2.go index 577fb94444..992a063b18 100644 --- a/server/oauth2.go +++ b/server/oauth2.go @@ -128,6 +128,7 @@ const ( grantTypeRefreshToken = "refresh_token" grantTypePassword = "password" grantTypeDeviceCode = "urn:ietf:params:oauth:grant-type:device_code" + grantTypeClientCredentials = "client_credentials" ) const ( From b1718e0e2229bb99d863a7a51dfd501b1099f5bb Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Wed, 15 Jan 2020 13:04:48 -0500 Subject: [PATCH 23/30] Use http.FileSystem for web assets Signed-off-by: Rui Yang Co-authored-by: Aidan Oldershaw --- cmd/dex/config_test.go | 4 - server/handlers_test.go | 2 +- server/server.go | 19 +--- server/server_test.go | 4 +- server/templates.go | 189 ++++++++++++++++---------------------- web/templates/header.html | 4 +- 6 files changed, 89 insertions(+), 133 deletions(-) diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 8ee02d5aa2..9a36afcf78 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -74,7 +74,6 @@ web: http: 127.0.0.1:5556 frontend: - dir: ./web extra: foo: bar @@ -144,7 +143,6 @@ logger: HTTP: "127.0.0.1:5556", }, Frontend: server.WebConfig{ - Dir: "./web", Extra: map[string]string{ "foo": "bar", }, @@ -274,7 +272,6 @@ web: http: 127.0.0.1:5556 frontend: - dir: ./web extra: foo: bar @@ -352,7 +349,6 @@ logger: HTTP: "127.0.0.1:5556", }, Frontend: server.WebConfig{ - Dir: "./web", Extra: map[string]string{ "foo": "bar", }, diff --git a/server/handlers_test.go b/server/handlers_test.go index 8ad59d947b..96d228afdf 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -135,7 +135,7 @@ func TestHandleInvalidSAMLCallbacks(t *testing.T) { func TestConnectorLoginDoesNotAllowToChangeConnectorForAuthRequest(t *testing.T) { memStorage := memory.New(logger) - templates, err := loadTemplates(webConfig{}, "../web/templates") + templates, err := loadTemplates(WebConfig{Dir: http.Dir("../web")}, "../web/templates") if err != nil { t.Fatal("failed to load templates") } diff --git a/server/server.go b/server/server.go index a79b7cfd3b..79c5f5a00f 100644 --- a/server/server.go +++ b/server/server.go @@ -108,7 +108,7 @@ type WebConfig struct { // * templates - HTML templates controlled by dex. // * themes/(theme) - Static static served at "( issuer URL )/theme". // - Dir string + Dir http.FileSystem // Defaults to "( issuer URL )/theme/logo.png" LogoURL string @@ -203,18 +203,9 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) supported[respType] = true } - web := webConfig{ - dir: c.Web.Dir, - logoURL: c.Web.LogoURL, - issuerURL: c.Issuer, - issuer: c.Web.Issuer, - theme: c.Web.Theme, - extra: c.Web.Extra, - } - - static, theme, tmpls, err := loadWebConfig(web) + tmpls, err := loadTemplates(c.Web, issuerURL.Path) if err != nil { - return nil, fmt.Errorf("server: failed to load web static: %v", err) + return nil, fmt.Errorf("server: failed to load templates: %v", err) } now := c.Now @@ -343,8 +334,8 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) } fmt.Fprintf(w, "Health check passed") })) - handlePrefix("/static", static) - handlePrefix("/theme", theme) + handlePrefix("/", http.FileServer(c.Web.Dir)) + s.mux = r s.startKeyRotation(ctx, rotationStrategy, now) diff --git a/server/server_test.go b/server/server_test.go index 87ca6c171e..b0bacfd362 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -93,7 +93,7 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi Issuer: s.URL, Storage: memory.New(logger), Web: WebConfig{ - Dir: "../web", + Dir: http.Dir("../web"), }, Logger: logger, PrometheusRegistry: prometheus.NewRegistry(), @@ -132,7 +132,7 @@ func newTestServerMultipleConnectors(ctx context.Context, t *testing.T, updateCo Issuer: s.URL, Storage: memory.New(logger), Web: WebConfig{ - Dir: "../web", + Dir: http.Dir("../web"), }, Logger: logger, PrometheusRegistry: prometheus.NewRegistry(), diff --git a/server/templates.go b/server/templates.go index bed1c6c86c..b23083ba9c 100644 --- a/server/templates.go +++ b/server/templates.go @@ -1,13 +1,12 @@ package server import ( + "bytes" "fmt" "html/template" "io" - "io/ioutil" "net/http" "net/url" - "os" "path" "path/filepath" "sort" @@ -22,18 +21,10 @@ const ( tmplError = "error.html" tmplDevice = "device.html" tmplDeviceSuccess = "device_success.html" + tmplHeader = "header.html" + tmplFooter = "footer.html" ) -var requiredTmpls = []string{ - tmplApproval, - tmplLogin, - tmplPassword, - tmplOOB, - tmplError, - tmplDevice, - tmplDeviceSuccess, -} - type templates struct { loginTmpl *template.Template approvalTmpl *template.Template @@ -44,131 +35,93 @@ type templates struct { deviceSuccessTmpl *template.Template } -type webConfig struct { - dir string - logoURL string - issuer string - theme string - issuerURL string - extra map[string]string -} - -func dirExists(dir string) error { - stat, err := os.Stat(dir) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("directory %q does not exist", dir) - } - return fmt.Errorf("stat directory %q: %v", dir, err) - } - if !stat.IsDir() { - return fmt.Errorf("path %q is a file not a directory", dir) - } - return nil -} - -// loadWebConfig returns static assets, theme assets, and templates used by the frontend by -// reading the directory specified in the webConfig. -// -// The directory layout is expected to be: -// -// ( web directory ) -// |- static -// |- themes -// | |- (theme name) -// |- templates -// -func loadWebConfig(c webConfig) (http.Handler, http.Handler, *templates, error) { +// loadTemplates parses the expected templates from the provided directory. +func loadTemplates(c WebConfig, issuerPath string) (*templates, error) { // fallback to the default theme if the legacy theme name is provided - if c.theme == "coreos" || c.theme == "tectonic" { - c.theme = "" - } - if c.theme == "" { - c.theme = "light" - } - if c.issuer == "" { - c.issuer = "dex" + if c.Theme == "coreos" || c.Theme == "tectonic" { + c.Theme = "" } - if c.dir == "" { - c.dir = "./web" - } - if c.logoURL == "" { - c.logoURL = "theme/logo.png" + if c.Theme == "" { + c.Theme = "light" } - if err := dirExists(c.dir); err != nil { - return nil, nil, nil, fmt.Errorf("load web dir: %v", err) + if c.Issuer == "" { + c.Issuer = "dex" } - staticDir := filepath.Join(c.dir, "static") - templatesDir := filepath.Join(c.dir, "templates") - themeDir := filepath.Join(c.dir, "themes", c.theme) + if c.LogoURL == "" { + c.LogoURL = "theme/logo.png" + } - for _, dir := range []string{staticDir, templatesDir, themeDir} { - if err := dirExists(dir); err != nil { - return nil, nil, nil, fmt.Errorf("load dir: %v", err) - } + funcs := template.FuncMap{ + "issuer": func() string { return c.Issuer }, + "logo": func() string { return c.LogoURL }, + "url": func(reqPath, assetPath string) string { return relativeURL(issuerPath, reqPath, assetPath) }, + "theme": func(reqPath, assetPath string) string { + return relativeURL(issuerPath, reqPath, path.Join("themes", c.Theme, assetPath)) + }, + "lower": strings.ToLower, + "extra": func(k string) string { return c.Extra[k] }, } - static := http.FileServer(http.Dir(staticDir)) - theme := http.FileServer(http.Dir(themeDir)) + group := template.New("") - templates, err := loadTemplates(c, templatesDir) - return static, theme, templates, err -} + // load all of our templates individually. + // some http.FilSystem implementations don't implement Readdir -// loadTemplates parses the expected templates from the provided directory. -func loadTemplates(c webConfig, templatesDir string) (*templates, error) { - files, err := ioutil.ReadDir(templatesDir) + loginTemplate, err := loadTemplate(c.Dir, tmplLogin, funcs, group) if err != nil { - return nil, fmt.Errorf("read dir: %v", err) + return nil, err } - filenames := []string{} - for _, file := range files { - if file.IsDir() { - continue - } - filenames = append(filenames, filepath.Join(templatesDir, file.Name())) + approvalTemplate, err := loadTemplate(c.Dir, tmplApproval, funcs, group) + if err != nil { + return nil, err } - if len(filenames) == 0 { - return nil, fmt.Errorf("no files in template dir %q", templatesDir) + + passwordTemplate, err := loadTemplate(c.Dir, tmplPassword, funcs, group) + if err != nil { + return nil, err } - issuerURL, err := url.Parse(c.issuerURL) + oobTemplate, err := loadTemplate(c.Dir, tmplOOB, funcs, group) if err != nil { - return nil, fmt.Errorf("error parsing issuerURL: %v", err) + return nil, err } - funcs := map[string]interface{}{ - "issuer": func() string { return c.issuer }, - "logo": func() string { return c.logoURL }, - "url": func(reqPath, assetPath string) string { return relativeURL(issuerURL.Path, reqPath, assetPath) }, - "lower": strings.ToLower, - "extra": func(k string) string { return c.extra[k] }, + errorTemplate, err := loadTemplate(c.Dir, tmplError, funcs, group) + if err != nil { + return nil, err } - tmpls, err := template.New("").Funcs(funcs).ParseFiles(filenames...) + deviceTemplate, err := loadTemplate(c.Dir, tmplDevice, funcs, group) if err != nil { - return nil, fmt.Errorf("parse files: %v", err) + return nil, err } - missingTmpls := []string{} - for _, tmplName := range requiredTmpls { - if tmpls.Lookup(tmplName) == nil { - missingTmpls = append(missingTmpls, tmplName) - } + + deviceSuccessTemplate, err := loadTemplate(c.Dir, tmplDeviceSuccess, funcs, group) + if err != nil { + return nil, err } - if len(missingTmpls) > 0 { - return nil, fmt.Errorf("missing template(s): %s", missingTmpls) + + _, err = loadTemplate(c.Dir, tmplHeader, funcs, group) + if err != nil { + // we don't actually care if this template exists } + + _, err = loadTemplate(c.Dir, tmplFooter, funcs, group) + if err != nil { + // we don't actually care if this template exists + } + return &templates{ - loginTmpl: tmpls.Lookup(tmplLogin), - approvalTmpl: tmpls.Lookup(tmplApproval), - passwordTmpl: tmpls.Lookup(tmplPassword), - oobTmpl: tmpls.Lookup(tmplOOB), - errorTmpl: tmpls.Lookup(tmplError), - deviceTmpl: tmpls.Lookup(tmplDevice), - deviceSuccessTmpl: tmpls.Lookup(tmplDeviceSuccess), + loginTmpl: loginTemplate, + approvalTmpl: approvalTemplate, + passwordTmpl: passwordTemplate, + oobTmpl: oobTemplate, + errorTmpl: errorTemplate, + deviceTmpl: deviceTemplate, + deviceSuccessTmpl: deviceSuccessTemplate, }, nil } @@ -239,6 +192,22 @@ func relativeURL(serverPath, reqPath, assetPath string) string { return relativeURL } +// load a template by name from the templates dir +func loadTemplate(dir http.FileSystem, name string, funcs template.FuncMap, group *template.Template) (*template.Template, error) { + file, err := dir.Open(filepath.Join("templates", name)) + if err != nil { + return nil, err + } + + defer file.Close() + + var buffer bytes.Buffer + buffer.ReadFrom(file) + contents := buffer.String() + + return group.New(name).Funcs(funcs).Parse(contents) +} + var scopeDescriptions = map[string]string{ "offline_access": "Have offline access", "profile": "View basic profile information", diff --git a/web/templates/header.html b/web/templates/header.html index 0d4fea0fa2..78dde15e0a 100644 --- a/web/templates/header.html +++ b/web/templates/header.html @@ -6,8 +6,8 @@ {{ issuer }} - - + + From adad4190ee598b769580783ab29314ea587c9ed8 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 21 Sep 2020 23:26:35 -0400 Subject: [PATCH 24/30] use web host url for asset hosting Signed-off-by: Rui Yang Co-authored-by: Aidan Oldershaw --- server/server.go | 3 +++ server/templates.go | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index 79c5f5a00f..baf2b30a99 100644 --- a/server/server.go +++ b/server/server.go @@ -121,6 +121,9 @@ type WebConfig struct { // Map of extra values passed into the templates Extra map[string]string + + // Defaults to issuer URL + HostURL string } func value(val, defaultValue time.Duration) time.Duration { diff --git a/server/templates.go b/server/templates.go index b23083ba9c..1c04a68eb5 100644 --- a/server/templates.go +++ b/server/templates.go @@ -53,12 +53,17 @@ func loadTemplates(c WebConfig, issuerPath string) (*templates, error) { c.LogoURL = "theme/logo.png" } + hostURL := issuerPath + if c.HostURL != "" { + hostURL = c.HostURL + } + funcs := template.FuncMap{ "issuer": func() string { return c.Issuer }, "logo": func() string { return c.LogoURL }, - "url": func(reqPath, assetPath string) string { return relativeURL(issuerPath, reqPath, assetPath) }, + "url": func(reqPath, assetPath string) string { return relativeURL(hostURL, reqPath, assetPath) }, "theme": func(reqPath, assetPath string) string { - return relativeURL(issuerPath, reqPath, path.Join("themes", c.Theme, assetPath)) + return relativeURL(hostURL, reqPath, path.Join("themes", c.Theme, assetPath)) }, "lower": strings.ToLower, "extra": func(k string) string { return c.Extra[k] }, From 3ef8663e3c4c7fdcbb7b433ef4dd21af8fe42d7a Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Wed, 14 Oct 2020 21:19:23 -0400 Subject: [PATCH 25/30] use pkger for embedding static contents Co-authored-by: Vikram Yadav Signed-off-by: Rui Yang --- Makefile | 1 + cmd/dex/config_test.go | 3 ++ cmd/dex/main.go | 2 + go.mod | 1 + go.sum | 16 +++----- server/handlers_test.go | 2 +- server/server.go | 13 ++++++- server/server_test.go | 4 +- server/templates.go | 82 +++++---------------------------------- server/templates_test.go | 51 ------------------------ web/templates/header.html | 8 ++-- 11 files changed, 39 insertions(+), 144 deletions(-) delete mode 100644 server/templates_test.go diff --git a/Makefile b/Makefile index 82b266a668..c5bb237a18 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ build: bin/dex bin/dex: @mkdir -p bin/ + @go generate ./... @go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex examples: bin/grpc-client bin/example-app diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 9a36afcf78..c62d18b56b 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -74,6 +74,7 @@ web: http: 127.0.0.1:5556 frontend: + dir: /web extra: foo: bar @@ -143,6 +144,7 @@ logger: HTTP: "127.0.0.1:5556", }, Frontend: server.WebConfig{ + Dir: "/web", Extra: map[string]string{ "foo": "bar", }, @@ -349,6 +351,7 @@ logger: HTTP: "127.0.0.1:5556", }, Frontend: server.WebConfig{ + Dir: "/web", Extra: map[string]string{ "foo": "bar", }, diff --git a/cmd/dex/main.go b/cmd/dex/main.go index be334d9207..978bcea12d 100644 --- a/cmd/dex/main.go +++ b/cmd/dex/main.go @@ -1,3 +1,5 @@ +//go:generate pkger + package main import ( diff --git a/go.mod b/go.mod index 6291284cca..6fd434c6d9 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/kylelemons/godebug v1.1.0 github.com/lib/pq v1.10.0 + github.com/markbates/pkger v0.17.1 github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1 github.com/mattn/go-sqlite3 v1.14.6 github.com/oklog/run v1.1.0 diff --git a/go.sum b/go.sum index 69b3bcf4d8..36971d116b 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -188,6 +189,8 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= +github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1 h1:x37Q11fexMtlhecRnkdzLL6dgnS1NF1nzAJ1vic22BY= github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -264,7 +267,6 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -280,7 +282,6 @@ github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -293,7 +294,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -339,7 +339,6 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -363,13 +362,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200505041828-1ed23360d12c h1:zJ0mtu4jCalhKg6Oaukv6iIkb+cOvDrajDH9DH46Q4M= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -378,7 +375,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -408,7 +404,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -456,7 +451,6 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4= @@ -493,6 +487,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= @@ -501,7 +496,6 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/testing_frameworks v0.1.2 h1:vK0+tvjF0BZ/RYFeZ1E6BYBwHJJXhjuZ3TdsEKH+UQM= diff --git a/server/handlers_test.go b/server/handlers_test.go index 96d228afdf..d52a45c016 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -135,7 +135,7 @@ func TestHandleInvalidSAMLCallbacks(t *testing.T) { func TestConnectorLoginDoesNotAllowToChangeConnectorForAuthRequest(t *testing.T) { memStorage := memory.New(logger) - templates, err := loadTemplates(WebConfig{Dir: http.Dir("../web")}, "../web/templates") + templates, err := loadTemplates(WebConfig{Dir: "/web"}, "/web/templates") if err != nil { t.Fatal("failed to load templates") } diff --git a/server/server.go b/server/server.go index baf2b30a99..df81a253e6 100644 --- a/server/server.go +++ b/server/server.go @@ -19,6 +19,7 @@ import ( "github.com/felixge/httpsnoop" "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/markbates/pkger" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/bcrypt" @@ -108,7 +109,7 @@ type WebConfig struct { // * templates - HTML templates controlled by dex. // * themes/(theme) - Static static served at "( issuer URL )/theme". // - Dir http.FileSystem + Dir string // Defaults to "( issuer URL )/theme/logo.png" LogoURL string @@ -206,6 +207,10 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) supported[respType] = true } + if c.Web.Dir == "" { + c.Web.Dir = pkger.Include("/web") + } + tmpls, err := loadTemplates(c.Web, issuerURL.Path) if err != nil { return nil, fmt.Errorf("server: failed to load templates: %v", err) @@ -337,7 +342,11 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) } fmt.Fprintf(w, "Health check passed") })) - handlePrefix("/", http.FileServer(c.Web.Dir)) + + staticDir := path.Join(c.Web.Dir, "static") + themeDir := path.Join(c.Web.Dir, "themes", c.Web.Theme) + handlePrefix("/static", http.FileServer(pkger.Dir(staticDir))) + handlePrefix(path.Join("/themes", c.Web.Theme), http.FileServer(pkger.Dir(themeDir))) s.mux = r diff --git a/server/server_test.go b/server/server_test.go index b0bacfd362..03bedbe60f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -93,7 +93,7 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi Issuer: s.URL, Storage: memory.New(logger), Web: WebConfig{ - Dir: http.Dir("../web"), + Dir: "/web", }, Logger: logger, PrometheusRegistry: prometheus.NewRegistry(), @@ -132,7 +132,7 @@ func newTestServerMultipleConnectors(ctx context.Context, t *testing.T, updateCo Issuer: s.URL, Storage: memory.New(logger), Web: WebConfig{ - Dir: http.Dir("../web"), + Dir: "/web", }, Logger: logger, PrometheusRegistry: prometheus.NewRegistry(), diff --git a/server/templates.go b/server/templates.go index 1c04a68eb5..49f70d1dd0 100644 --- a/server/templates.go +++ b/server/templates.go @@ -6,11 +6,12 @@ import ( "html/template" "io" "net/http" - "net/url" "path" "path/filepath" "sort" "strings" + + "github.com/markbates/pkger" ) const ( @@ -61,9 +62,11 @@ func loadTemplates(c WebConfig, issuerPath string) (*templates, error) { funcs := template.FuncMap{ "issuer": func() string { return c.Issuer }, "logo": func() string { return c.LogoURL }, - "url": func(reqPath, assetPath string) string { return relativeURL(hostURL, reqPath, assetPath) }, - "theme": func(reqPath, assetPath string) string { - return relativeURL(hostURL, reqPath, path.Join("themes", c.Theme, assetPath)) + "static": func(assetPath string) string { + return path.Join(hostURL, "static", assetPath) + }, + "theme": func(assetPath string) string { + return path.Join(hostURL, "themes", c.Theme, assetPath) }, "lower": strings.ToLower, "extra": func(k string) string { return c.Extra[k] }, @@ -130,76 +133,9 @@ func loadTemplates(c WebConfig, issuerPath string) (*templates, error) { }, nil } -// relativeURL returns the URL of the asset relative to the URL of the request path. -// The serverPath is consulted to trim any prefix due in case it is not listening -// to the root path. -// -// Algorithm: -// 1. Remove common prefix of serverPath and reqPath -// 2. Remove common prefix of assetPath and reqPath -// 3. For each part of reqPath remaining(minus one), go up one level (..) -// 4. For each part of assetPath remaining, append it to result -// -// eg -// server listens at localhost/dex so serverPath is dex -// reqPath is /dex/auth -// assetPath is static/main.css -// relativeURL("/dex", "/dex/auth", "static/main.css") = "../static/main.css" -func relativeURL(serverPath, reqPath, assetPath string) string { - if u, err := url.ParseRequestURI(assetPath); err == nil && u.Scheme != "" { - // assetPath points to the external URL, no changes needed - return assetPath - } - - splitPath := func(p string) []string { - res := []string{} - parts := strings.Split(path.Clean(p), "/") - for _, part := range parts { - if part != "" { - res = append(res, part) - } - } - return res - } - - stripCommonParts := func(s1, s2 []string) ([]string, []string) { - min := len(s1) - if len(s2) < min { - min = len(s2) - } - - splitIndex := min - for i := 0; i < min; i++ { - if s1[i] != s2[i] { - splitIndex = i - break - } - } - return s1[splitIndex:], s2[splitIndex:] - } - - server, req, asset := splitPath(serverPath), splitPath(reqPath), splitPath(assetPath) - - // Remove common prefix of request path with server path - _, req = stripCommonParts(server, req) - - // Remove common prefix of request path with asset path - asset, req = stripCommonParts(asset, req) - - // For each part of the request remaining (minus one) -> go up one level (..) - // For each part of the asset remaining -> append it - var relativeURL string - for i := 0; i < len(req)-1; i++ { - relativeURL = path.Join("..", relativeURL) - } - relativeURL = path.Join(relativeURL, path.Join(asset...)) - - return relativeURL -} - // load a template by name from the templates dir -func loadTemplate(dir http.FileSystem, name string, funcs template.FuncMap, group *template.Template) (*template.Template, error) { - file, err := dir.Open(filepath.Join("templates", name)) +func loadTemplate(dir string, name string, funcs template.FuncMap, group *template.Template) (*template.Template, error) { + file, err := pkger.Open(filepath.Join(dir, "templates", name)) if err != nil { return nil, err } diff --git a/server/templates_test.go b/server/templates_test.go deleted file mode 100644 index defb2e5e7d..0000000000 --- a/server/templates_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package server - -import "testing" - -func TestRelativeURL(t *testing.T) { - tests := []struct { - name string - serverPath string - reqPath string - assetPath string - expected string - }{ - { - name: "server-root-req-one-level-asset-two-level", - serverPath: "/", - reqPath: "/auth", - assetPath: "/theme/main.css", - expected: "theme/main.css", - }, - { - name: "server-one-level-req-one-level-asset-two-level", - serverPath: "/dex", - reqPath: "/dex/auth", - assetPath: "/theme/main.css", - expected: "theme/main.css", - }, - { - name: "server-root-req-two-level-asset-three-level", - serverPath: "/dex", - reqPath: "/dex/auth/connector", - assetPath: "assets/css/main.css", - expected: "../assets/css/main.css", - }, - { - name: "external-url", - serverPath: "/dex", - reqPath: "/dex/auth/connector", - assetPath: "https://kubernetes.io/images/favicon.png", - expected: "https://kubernetes.io/images/favicon.png", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actual := relativeURL(test.serverPath, test.reqPath, test.assetPath) - if actual != test.expected { - t.Fatalf("Got '%s'. Expected '%s'", actual, test.expected) - } - }) - } -} diff --git a/web/templates/header.html b/web/templates/header.html index 78dde15e0a..bbd1c13fa6 100644 --- a/web/templates/header.html +++ b/web/templates/header.html @@ -5,15 +5,15 @@ {{ issuer }} - - - + + +
- +
From 22aaf89add9056ddf5877ec9fcbb8b1bf54e1ff5 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Thu, 15 Oct 2020 10:50:39 -0400 Subject: [PATCH 26/30] use go 1.16 new package io/fs Unify the interface for reading web statics. Now it could read an OS directory or get the content on live One could use //go:embed static var webFiles embed.FS anywhere and config dex server to take the file system by setting WebConfig{WebFS: webFiles} Signed-off-by: Rui Yang Co-authored-by: Aidan Oldershaw --- Makefile | 1 - cmd/dex/config_test.go | 7 +- cmd/dex/main.go | 2 - go.mod | 1 - go.sum | 10 -- server/handlers_test.go | 3 +- server/server.go | 32 +++--- server/server_test.go | 4 +- server/templates.go | 235 +++++++++++++++++++++++++------------- server/templates_test.go | 51 +++++++++ web/templates/header.html | 9 +- 11 files changed, 235 insertions(+), 120 deletions(-) create mode 100644 server/templates_test.go diff --git a/Makefile b/Makefile index c5bb237a18..82b266a668 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,6 @@ build: bin/dex bin/dex: @mkdir -p bin/ - @go generate ./... @go install -v -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/dex examples: bin/grpc-client bin/example-app diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index c62d18b56b..8ee02d5aa2 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -74,7 +74,7 @@ web: http: 127.0.0.1:5556 frontend: - dir: /web + dir: ./web extra: foo: bar @@ -144,7 +144,7 @@ logger: HTTP: "127.0.0.1:5556", }, Frontend: server.WebConfig{ - Dir: "/web", + Dir: "./web", Extra: map[string]string{ "foo": "bar", }, @@ -274,6 +274,7 @@ web: http: 127.0.0.1:5556 frontend: + dir: ./web extra: foo: bar @@ -351,7 +352,7 @@ logger: HTTP: "127.0.0.1:5556", }, Frontend: server.WebConfig{ - Dir: "/web", + Dir: "./web", Extra: map[string]string{ "foo": "bar", }, diff --git a/cmd/dex/main.go b/cmd/dex/main.go index 978bcea12d..be334d9207 100644 --- a/cmd/dex/main.go +++ b/cmd/dex/main.go @@ -1,5 +1,3 @@ -//go:generate pkger - package main import ( diff --git a/go.mod b/go.mod index 6fd434c6d9..6291284cca 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/kylelemons/godebug v1.1.0 github.com/lib/pq v1.10.0 - github.com/markbates/pkger v0.17.1 github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1 github.com/mattn/go-sqlite3 v1.14.6 github.com/oklog/run v1.1.0 diff --git a/go.sum b/go.sum index 36971d116b..3e624f53f1 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= @@ -15,7 +14,6 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AppsFlyer/go-sundheit v0.3.1 h1:Zqnr3wV3WQmXonc234k9XZAoV2KHUHw3osR5k2iHQZE= github.com/AppsFlyer/go-sundheit v0.3.1/go.mod h1:iZ8zWMS7idcvmqewf5mEymWWgoOiG/0WD4+aeh+heX4= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -42,9 +40,7 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible h1:8F3hqu9fGYLBifCmRCJsicFqDx/D68Rt3q1JMazcgBQ= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ= github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= @@ -74,7 +70,6 @@ github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8S github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -86,8 +81,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= -github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= @@ -189,8 +182,6 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= -github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1 h1:x37Q11fexMtlhecRnkdzLL6dgnS1NF1nzAJ1vic22BY= github.com/mattermost/xml-roundtrip-validator v0.0.0-20201219040909-8fd2afad43d1/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -487,7 +478,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/server/handlers_test.go b/server/handlers_test.go index d52a45c016..d195af64b4 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -7,6 +7,7 @@ import ( "errors" "net/http" "net/http/httptest" + "os" "testing" "time" @@ -135,7 +136,7 @@ func TestHandleInvalidSAMLCallbacks(t *testing.T) { func TestConnectorLoginDoesNotAllowToChangeConnectorForAuthRequest(t *testing.T) { memStorage := memory.New(logger) - templates, err := loadTemplates(WebConfig{Dir: "/web"}, "/web/templates") + templates, err := loadTemplates(webConfig{webFS: os.DirFS("../web")}, "templates") if err != nil { t.Fatal("failed to load templates") } diff --git a/server/server.go b/server/server.go index df81a253e6..46b2377169 100644 --- a/server/server.go +++ b/server/server.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "net/http" "net/url" "path" @@ -19,7 +20,6 @@ import ( "github.com/felixge/httpsnoop" "github.com/gorilla/handlers" "github.com/gorilla/mux" - "github.com/markbates/pkger" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/bcrypt" @@ -101,7 +101,7 @@ type Config struct { // WebConfig holds the server's frontend templates and asset configuration. type WebConfig struct { - // A filepath to web static. + // A file path to web static. If set, WebFS will be ignored. // // It is expected to contain the following directories: // @@ -111,6 +111,10 @@ type WebConfig struct { // Dir string + // A file system includes web static. Will be overwritten by Dir + // It is expected to contain the directories as Dir. + WebFS fs.FS + // Defaults to "( issuer URL )/theme/logo.png" LogoURL string @@ -122,9 +126,6 @@ type WebConfig struct { // Map of extra values passed into the templates Extra map[string]string - - // Defaults to issuer URL - HostURL string } func value(val, defaultValue time.Duration) time.Duration { @@ -207,13 +208,19 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) supported[respType] = true } - if c.Web.Dir == "" { - c.Web.Dir = pkger.Include("/web") + web := webConfig{ + dir: c.Web.Dir, + webFS: c.Web.WebFS, + logoURL: c.Web.LogoURL, + issuerURL: c.Issuer, + issuer: c.Web.Issuer, + theme: c.Web.Theme, + extra: c.Web.Extra, } - tmpls, err := loadTemplates(c.Web, issuerURL.Path) + static, theme, tmpls, err := loadWebConfig(web) if err != nil { - return nil, fmt.Errorf("server: failed to load templates: %v", err) + return nil, fmt.Errorf("server: failed to load web static: %v", err) } now := c.Now @@ -343,11 +350,8 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) fmt.Fprintf(w, "Health check passed") })) - staticDir := path.Join(c.Web.Dir, "static") - themeDir := path.Join(c.Web.Dir, "themes", c.Web.Theme) - handlePrefix("/static", http.FileServer(pkger.Dir(staticDir))) - handlePrefix(path.Join("/themes", c.Web.Theme), http.FileServer(pkger.Dir(themeDir))) - + handlePrefix("/static", static) + handlePrefix("/theme", theme) s.mux = r s.startKeyRotation(ctx, rotationStrategy, now) diff --git a/server/server_test.go b/server/server_test.go index 03bedbe60f..87ca6c171e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -93,7 +93,7 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi Issuer: s.URL, Storage: memory.New(logger), Web: WebConfig{ - Dir: "/web", + Dir: "../web", }, Logger: logger, PrometheusRegistry: prometheus.NewRegistry(), @@ -132,7 +132,7 @@ func newTestServerMultipleConnectors(ctx context.Context, t *testing.T, updateCo Issuer: s.URL, Storage: memory.New(logger), Web: WebConfig{ - Dir: "/web", + Dir: "../web", }, Logger: logger, PrometheusRegistry: prometheus.NewRegistry(), diff --git a/server/templates.go b/server/templates.go index 49f70d1dd0..0d19164427 100644 --- a/server/templates.go +++ b/server/templates.go @@ -1,17 +1,17 @@ package server import ( - "bytes" "fmt" "html/template" "io" + "io/fs" "net/http" + "net/url" + "os" "path" "path/filepath" "sort" "strings" - - "github.com/markbates/pkger" ) const ( @@ -22,10 +22,18 @@ const ( tmplError = "error.html" tmplDevice = "device.html" tmplDeviceSuccess = "device_success.html" - tmplHeader = "header.html" - tmplFooter = "footer.html" ) +var requiredTmpls = []string{ + tmplApproval, + tmplLogin, + tmplPassword, + tmplOOB, + tmplError, + tmplDevice, + tmplDeviceSuccess, +} + type templates struct { loginTmpl *template.Template approvalTmpl *template.Template @@ -36,117 +44,182 @@ type templates struct { deviceSuccessTmpl *template.Template } -// loadTemplates parses the expected templates from the provided directory. -func loadTemplates(c WebConfig, issuerPath string) (*templates, error) { +type webConfig struct { + dir string + webFS fs.FS + logoURL string + issuer string + theme string + issuerURL string + extra map[string]string +} + +// loadWebConfig returns static assets, theme assets, and templates used by the frontend by +// reading the dir specified in the webConfig. If directory is not specified it will +// use the file system specified by webFS. +// +// The directory layout is expected to be: +// +// ( web directory ) +// |- static +// |- themes +// | |- (theme name) +// |- templates +// +func loadWebConfig(c webConfig) (http.Handler, http.Handler, *templates, error) { // fallback to the default theme if the legacy theme name is provided - if c.Theme == "coreos" || c.Theme == "tectonic" { - c.Theme = "" + if c.theme == "coreos" || c.theme == "tectonic" { + c.theme = "" } - if c.Theme == "" { - c.Theme = "light" + if c.theme == "" { + c.theme = "light" } - - if c.Issuer == "" { - c.Issuer = "dex" + if c.issuer == "" { + c.issuer = "dex" } - - if c.LogoURL == "" { - c.LogoURL = "theme/logo.png" + if c.dir != "" { + c.webFS = os.DirFS(c.dir) } - - hostURL := issuerPath - if c.HostURL != "" { - hostURL = c.HostURL + if c.logoURL == "" { + c.logoURL = "theme/logo.png" } - funcs := template.FuncMap{ - "issuer": func() string { return c.Issuer }, - "logo": func() string { return c.LogoURL }, - "static": func(assetPath string) string { - return path.Join(hostURL, "static", assetPath) - }, - "theme": func(assetPath string) string { - return path.Join(hostURL, "themes", c.Theme, assetPath) - }, - "lower": strings.ToLower, - "extra": func(k string) string { return c.Extra[k] }, + staticFiles, err := fs.Sub(c.webFS, "static") + if err != nil { + return nil, nil, nil, fmt.Errorf("read static dir: %v", err) + } + themeFiles, err := fs.Sub(c.webFS, filepath.Join("themes", c.theme)) + if err != nil { + return nil, nil, nil, fmt.Errorf("read themes dir: %v", err) } - group := template.New("") + static := http.FileServer(http.FS(staticFiles)) + theme := http.FileServer(http.FS(themeFiles)) - // load all of our templates individually. - // some http.FilSystem implementations don't implement Readdir + templates, err := loadTemplates(c, "templates") + return static, theme, templates, err +} - loginTemplate, err := loadTemplate(c.Dir, tmplLogin, funcs, group) +// loadTemplates parses the expected templates from the provided directory. +func loadTemplates(c webConfig, templatesDir string) (*templates, error) { + files, err := fs.ReadDir(c.webFS, templatesDir) if err != nil { - return nil, err + return nil, fmt.Errorf("read dir: %v", err) } - approvalTemplate, err := loadTemplate(c.Dir, tmplApproval, funcs, group) - if err != nil { - return nil, err + filenames := []string{} + for _, file := range files { + if file.IsDir() { + continue + } + filenames = append(filenames, filepath.Join(templatesDir, file.Name())) } - - passwordTemplate, err := loadTemplate(c.Dir, tmplPassword, funcs, group) - if err != nil { - return nil, err + if len(filenames) == 0 { + return nil, fmt.Errorf("no files in template dir %q", templatesDir) } - oobTemplate, err := loadTemplate(c.Dir, tmplOOB, funcs, group) + issuerURL, err := url.Parse(c.issuerURL) if err != nil { - return nil, err + return nil, fmt.Errorf("error parsing issuerURL: %v", err) } - errorTemplate, err := loadTemplate(c.Dir, tmplError, funcs, group) - if err != nil { - return nil, err + funcs := map[string]interface{}{ + "issuer": func() string { return c.issuer }, + "logo": func() string { return c.logoURL }, + "url": func(reqPath, assetPath string) string { return relativeURL(issuerURL.Path, reqPath, assetPath) }, + "lower": strings.ToLower, + "extra": func(k string) string { return c.extra[k] }, } - deviceTemplate, err := loadTemplate(c.Dir, tmplDevice, funcs, group) + tmpls, err := template.New("").Funcs(funcs).ParseFS(c.webFS, filenames...) if err != nil { - return nil, err + return nil, fmt.Errorf("parse files: %v", err) } - - deviceSuccessTemplate, err := loadTemplate(c.Dir, tmplDeviceSuccess, funcs, group) - if err != nil { - return nil, err + missingTmpls := []string{} + for _, tmplName := range requiredTmpls { + if tmpls.Lookup(tmplName) == nil { + missingTmpls = append(missingTmpls, tmplName) + } } + if len(missingTmpls) > 0 { + return nil, fmt.Errorf("missing template(s): %s", missingTmpls) + } + return &templates{ + loginTmpl: tmpls.Lookup(tmplLogin), + approvalTmpl: tmpls.Lookup(tmplApproval), + passwordTmpl: tmpls.Lookup(tmplPassword), + oobTmpl: tmpls.Lookup(tmplOOB), + errorTmpl: tmpls.Lookup(tmplError), + deviceTmpl: tmpls.Lookup(tmplDevice), + deviceSuccessTmpl: tmpls.Lookup(tmplDeviceSuccess), + }, nil +} - _, err = loadTemplate(c.Dir, tmplHeader, funcs, group) - if err != nil { - // we don't actually care if this template exists +// relativeURL returns the URL of the asset relative to the URL of the request path. +// The serverPath is consulted to trim any prefix due in case it is not listening +// to the root path. +// +// Algorithm: +// 1. Remove common prefix of serverPath and reqPath +// 2. Remove common prefix of assetPath and reqPath +// 3. For each part of reqPath remaining(minus one), go up one level (..) +// 4. For each part of assetPath remaining, append it to result +// +// eg +// server listens at localhost/dex so serverPath is dex +// reqPath is /dex/auth +// assetPath is static/main.css +// relativeURL("/dex", "/dex/auth", "static/main.css") = "../static/main.css" +func relativeURL(serverPath, reqPath, assetPath string) string { + if u, err := url.ParseRequestURI(assetPath); err == nil && u.Scheme != "" { + // assetPath points to the external URL, no changes needed + return assetPath } - _, err = loadTemplate(c.Dir, tmplFooter, funcs, group) - if err != nil { - // we don't actually care if this template exists + splitPath := func(p string) []string { + res := []string{} + parts := strings.Split(path.Clean(p), "/") + for _, part := range parts { + if part != "" { + res = append(res, part) + } + } + return res } - return &templates{ - loginTmpl: loginTemplate, - approvalTmpl: approvalTemplate, - passwordTmpl: passwordTemplate, - oobTmpl: oobTemplate, - errorTmpl: errorTemplate, - deviceTmpl: deviceTemplate, - deviceSuccessTmpl: deviceSuccessTemplate, - }, nil -} + stripCommonParts := func(s1, s2 []string) ([]string, []string) { + min := len(s1) + if len(s2) < min { + min = len(s2) + } -// load a template by name from the templates dir -func loadTemplate(dir string, name string, funcs template.FuncMap, group *template.Template) (*template.Template, error) { - file, err := pkger.Open(filepath.Join(dir, "templates", name)) - if err != nil { - return nil, err + splitIndex := min + for i := 0; i < min; i++ { + if s1[i] != s2[i] { + splitIndex = i + break + } + } + return s1[splitIndex:], s2[splitIndex:] } - defer file.Close() + server, req, asset := splitPath(serverPath), splitPath(reqPath), splitPath(assetPath) + + // Remove common prefix of request path with server path + _, req = stripCommonParts(server, req) - var buffer bytes.Buffer - buffer.ReadFrom(file) - contents := buffer.String() + // Remove common prefix of request path with asset path + asset, req = stripCommonParts(asset, req) + + // For each part of the request remaining (minus one) -> go up one level (..) + // For each part of the asset remaining -> append it + var relativeURL string + for i := 0; i < len(req)-1; i++ { + relativeURL = path.Join("..", relativeURL) + } + relativeURL = path.Join(relativeURL, path.Join(asset...)) - return group.New(name).Funcs(funcs).Parse(contents) + return relativeURL } var scopeDescriptions = map[string]string{ diff --git a/server/templates_test.go b/server/templates_test.go new file mode 100644 index 0000000000..defb2e5e7d --- /dev/null +++ b/server/templates_test.go @@ -0,0 +1,51 @@ +package server + +import "testing" + +func TestRelativeURL(t *testing.T) { + tests := []struct { + name string + serverPath string + reqPath string + assetPath string + expected string + }{ + { + name: "server-root-req-one-level-asset-two-level", + serverPath: "/", + reqPath: "/auth", + assetPath: "/theme/main.css", + expected: "theme/main.css", + }, + { + name: "server-one-level-req-one-level-asset-two-level", + serverPath: "/dex", + reqPath: "/dex/auth", + assetPath: "/theme/main.css", + expected: "theme/main.css", + }, + { + name: "server-root-req-two-level-asset-three-level", + serverPath: "/dex", + reqPath: "/dex/auth/connector", + assetPath: "assets/css/main.css", + expected: "../assets/css/main.css", + }, + { + name: "external-url", + serverPath: "/dex", + reqPath: "/dex/auth/connector", + assetPath: "https://kubernetes.io/images/favicon.png", + expected: "https://kubernetes.io/images/favicon.png", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := relativeURL(test.serverPath, test.reqPath, test.assetPath) + if actual != test.expected { + t.Fatalf("Got '%s'. Expected '%s'", actual, test.expected) + } + }) + } +} diff --git a/web/templates/header.html b/web/templates/header.html index bbd1c13fa6..8cf744e51e 100644 --- a/web/templates/header.html +++ b/web/templates/header.html @@ -5,17 +5,16 @@ {{ issuer }} - - - + + +
- +
- From 31025eefa5d709fa05a0aa0c572ac14416ef269b Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Thu, 4 Mar 2021 10:17:05 -0500 Subject: [PATCH 27/30] default to ./web when Dir and WebFS are not set update WebFS doc Signed-off-by: Rui Yang Co-authored-by: Aidan Oldershaw --- server/server.go | 6 ++++-- server/templates.go | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index 46b2377169..17d72af393 100644 --- a/server/server.go +++ b/server/server.go @@ -111,8 +111,10 @@ type WebConfig struct { // Dir string - // A file system includes web static. Will be overwritten by Dir - // It is expected to contain the directories as Dir. + // Alternative way to configure web static filesystem. Dir overrides this. + // It's expected to contain the same files and directories as mentioned + // above in Dir doc. + // WebFS fs.FS // Defaults to "( issuer URL )/theme/logo.png" diff --git a/server/templates.go b/server/templates.go index 0d19164427..ca5e4d24be 100644 --- a/server/templates.go +++ b/server/templates.go @@ -79,6 +79,8 @@ func loadWebConfig(c webConfig) (http.Handler, http.Handler, *templates, error) } if c.dir != "" { c.webFS = os.DirFS(c.dir) + } else if c.webFS == nil { + c.webFS = os.DirFS("./web") } if c.logoURL == "" { c.logoURL = "theme/logo.png" From 14970ac545e65ad3b16ff777c2ecfd164ca87ea9 Mon Sep 17 00:00:00 2001 From: Alex Suraci Date: Tue, 14 Aug 2018 13:49:07 -0400 Subject: [PATCH 28/30] allow configuring CAs/skip verify for OIDC Co-authored-by: Rui Yang Signed-off-by: Alex Suraci --- connector/oidc/oidc.go | 58 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index b752f9dac5..8281d60a53 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -3,9 +3,13 @@ package oidc import ( "context" + "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" + "io/ioutil" + "net" "net/http" "net/url" "strings" @@ -38,6 +42,12 @@ type Config struct { // If this field is nonempty, only users from a listed domain will be allowed to log in HostedDomains []string `json:"hostedDomains"` + // Certificates for SSL validation + RootCAs []string `json:"rootCAs"` + + // Disable certificate verification + InsecureSkipVerify bool `json:"insecureSkipVerify"` + // Override the value of email_verified to true in the returned claims InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified"` @@ -99,7 +109,13 @@ func knownBrokenAuthHeaderProvider(issuerURL string) bool { // Open returns a connector which can be used to login users through an upstream // OpenID Connect provider. func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) { + httpClient, err := newHTTPClient(c.RootCAs, c.InsecureSkipVerify) + if err != nil { + return nil, err + } + ctx, cancel := context.WithCancel(context.Background()) + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) provider, err := oidc.NewProvider(ctx, c.Issuer) if err != nil { @@ -146,6 +162,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e ), logger: logger, cancel: cancel, + httpClient: httpClient, hostedDomains: c.HostedDomains, insecureSkipEmailVerified: c.InsecureSkipEmailVerified, insecureEnableGroups: c.InsecureEnableGroups, @@ -159,6 +176,40 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e }, nil } +func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) { + pool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} + for _, rootCA := range rootCAs { + rootCABytes, err := ioutil.ReadFile(rootCA) + if err != nil { + return nil, fmt.Errorf("failed to read root-ca: %v", err) + } + if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) { + return nil, fmt.Errorf("no certs found in root CA file %q", rootCA) + } + } + + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tlsConfig, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, nil +} + var ( _ connector.CallbackConnector = (*oidcConnector)(nil) _ connector.RefreshConnector = (*oidcConnector)(nil) @@ -171,6 +222,7 @@ type oidcConnector struct { verifier *oidc.IDTokenVerifier cancel context.CancelFunc logger log.Logger + httpClient *http.Client hostedDomains []string insecureSkipEmailVerified bool insecureEnableGroups bool @@ -225,7 +277,10 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (ide if errType := q.Get("error"); errType != "" { return identity, &oauth2Error{errType, q.Get("error_description")} } - token, err := c.oauth2Config.Exchange(r.Context(), q.Get("code")) + + ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient) + + token, err := c.oauth2Config.Exchange(ctx, q.Get("code")) if err != nil { return identity, fmt.Errorf("oidc: failed to get token: %v", err) } @@ -258,6 +313,7 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I if !ok { return identity, errors.New("oidc: no id_token in token response") } + idToken, err := c.verifier.Verify(ctx, rawIDToken) if err != nil { return identity, fmt.Errorf("oidc: failed to verify ID Token: %v", err) From ef54461d1bafeced0b69ba97e3bf3b3ceb8d3967 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Tue, 16 Mar 2021 18:37:47 +0000 Subject: [PATCH 29/30] upstream dex release: v2.28.0 The official docker release for this release can be pulled from ``` ghcr.io/dexidp/dex:v2.28.0 ``` **Features:** - Add c_hash to id_token, issued on /auth endpoint, when in hybrid flow (#1773, @HEllRZA) - Allow configuration of returned auth proxy header (#1839, @seuf) - Allow to disable os.ExpandEnv for storage + connector configs by env variable DEX_EXPAND_ENV = false (#1902, @heidemn-faro) - Added the possibility to activate lowercase for UPN-Strings (#1888, @VF-mbrauer) - Add "Cache-control: no-store" and "Pragma: no-cache" headers to token responses (#1948, @nabokihms) - Add gomplate to the docker image (#1893, @nabokihms) - Graceful shutdown (#1963, @nabokihms) - Allow public clients created with API to have no client_secret (#1871, @spohner) **Bugfixes:** - Fix the etcd PKCE AuthCode deserialization (#1908, @bnu0) - Fix garbage collection logging of device codes and device request (#1918, @nabokihms) - Discovery endpoint contains updated claims and auth methods (#1951, @nabokihms) - Return invalid_grant error if auth code is invalid or expired (#1952, @nabokihms) - Return an error to auth requests with the "request" parameter (#1956, @nabokihms) **Minor changes:** - Change default themes to light/dark (#1858, @nabokihms) - Various developer experience improvements - Dependency upgrades - Tons of small fixes and changes --- cmd/dex/config.go | 14 +++---- cmd/dex/config_test.go | 10 ++--- cmd/dex/serve.go | 6 +-- cmd/dex/version.go | 2 +- connector/atlassiancrowd/atlassiancrowd.go | 6 +-- connector/authproxy/authproxy.go | 4 +- connector/bitbucketcloud/bitbucketcloud.go | 6 +-- .../bitbucketcloud/bitbucketcloud_test.go | 2 +- connector/cf/cf.go | 4 +- connector/cf/cf_test.go | 2 +- connector/gitea/gitea.go | 4 +- connector/gitea/gitea_test.go | 2 +- connector/github/github.go | 6 +-- connector/github/github_test.go | 2 +- connector/gitlab/gitlab.go | 6 +-- connector/gitlab/gitlab_test.go | 2 +- connector/google/google.go | 6 +-- connector/keystone/keystone.go | 4 +- connector/keystone/keystone_test.go | 2 +- connector/ldap/ldap.go | 4 +- connector/ldap/ldap_test.go | 2 +- connector/linkedin/linkedin.go | 4 +- connector/microsoft/microsoft.go | 6 +-- connector/microsoft/microsoft_test.go | 2 +- connector/mock/connectortest.go | 4 +- connector/oauth/oauth.go | 4 +- connector/oauth/oauth_test.go | 2 +- connector/oidc/oidc.go | 4 +- connector/oidc/oidc_test.go | 2 +- connector/openshift/openshift.go | 8 ++-- connector/openshift/openshift_test.go | 4 +- connector/saml/saml.go | 6 +-- connector/saml/saml_test.go | 2 +- examples/go.mod | 2 +- go.mod | 2 +- pkg/groups/groups_test.go | 2 +- server/api.go | 8 ++-- server/api_test.go | 8 ++-- server/deviceflowhandlers.go | 2 +- server/deviceflowhandlers_test.go | 2 +- server/handlers.go | 6 +-- server/handlers_test.go | 4 +- server/oauth2.go | 6 +-- server/oauth2_test.go | 4 +- server/rotation.go | 4 +- server/rotation_test.go | 4 +- server/server.go | 40 +++++++++---------- server/server_test.go | 8 ++-- storage/conformance/conformance.go | 2 +- storage/conformance/transactions.go | 2 +- storage/etcd/config.go | 4 +- storage/etcd/etcd.go | 4 +- storage/etcd/etcd_test.go | 4 +- storage/etcd/types.go | 2 +- storage/kubernetes/client.go | 6 +-- storage/kubernetes/storage.go | 6 +-- storage/kubernetes/storage_test.go | 4 +- storage/kubernetes/types.go | 4 +- storage/memory/memory.go | 4 +- storage/memory/memory_test.go | 4 +- storage/memory/static_test.go | 2 +- storage/sql/config.go | 4 +- storage/sql/config_test.go | 6 +-- storage/sql/crud.go | 2 +- storage/sql/sql.go | 2 +- storage/sql/sqlite.go | 4 +- storage/static.go | 2 +- 67 files changed, 157 insertions(+), 157 deletions(-) diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 88dc98e720..60c9d8ba26 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -10,13 +10,13 @@ import ( "golang.org/x/crypto/bcrypt" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/server" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/etcd" - "github.com/dexidp/dex/storage/kubernetes" - "github.com/dexidp/dex/storage/memory" - "github.com/dexidp/dex/storage/sql" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/server" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/etcd" + "github.com/concourse/dex/storage/kubernetes" + "github.com/concourse/dex/storage/memory" + "github.com/concourse/dex/storage/sql" ) // Config is the config format for the main application. diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 8ee02d5aa2..2f7bc8447e 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -7,11 +7,11 @@ import ( "github.com/ghodss/yaml" "github.com/kylelemons/godebug/pretty" - "github.com/dexidp/dex/connector/mock" - "github.com/dexidp/dex/connector/oidc" - "github.com/dexidp/dex/server" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/sql" + "github.com/concourse/dex/connector/mock" + "github.com/concourse/dex/connector/oidc" + "github.com/concourse/dex/server" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/sql" ) var _ = yaml.YAMLToJSON diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index bd7869c427..6e8d0081dc 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -29,9 +29,9 @@ import ( "google.golang.org/grpc/reflection" "github.com/dexidp/dex/api/v2" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/server" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/server" + "github.com/concourse/dex/storage" ) type serveOptions struct { diff --git a/cmd/dex/version.go b/cmd/dex/version.go index de206e16d8..85ff30e583 100644 --- a/cmd/dex/version.go +++ b/cmd/dex/version.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" - "github.com/dexidp/dex/version" + "github.com/concourse/dex/version" ) func commandVersion() *cobra.Command { diff --git a/connector/atlassiancrowd/atlassiancrowd.go b/connector/atlassiancrowd/atlassiancrowd.go index 6f03406086..f37b7db574 100644 --- a/connector/atlassiancrowd/atlassiancrowd.go +++ b/connector/atlassiancrowd/atlassiancrowd.go @@ -13,9 +13,9 @@ import ( "strings" "time" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" ) // Config holds configuration options for Atlassian Crowd connector. diff --git a/connector/authproxy/authproxy.go b/connector/authproxy/authproxy.go index 853e5ee29f..db71149e64 100644 --- a/connector/authproxy/authproxy.go +++ b/connector/authproxy/authproxy.go @@ -8,8 +8,8 @@ import ( "net/http" "net/url" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) // Config holds the configuration parameters for a connector which returns an diff --git a/connector/bitbucketcloud/bitbucketcloud.go b/connector/bitbucketcloud/bitbucketcloud.go index e81893da07..0f9adb916d 100644 --- a/connector/bitbucketcloud/bitbucketcloud.go +++ b/connector/bitbucketcloud/bitbucketcloud.go @@ -14,9 +14,9 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/bitbucket" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" ) const ( diff --git a/connector/bitbucketcloud/bitbucketcloud_test.go b/connector/bitbucketcloud/bitbucketcloud_test.go index 3d984a8fcb..8c7b3858c6 100644 --- a/connector/bitbucketcloud/bitbucketcloud_test.go +++ b/connector/bitbucketcloud/bitbucketcloud_test.go @@ -10,7 +10,7 @@ import ( "reflect" "testing" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) func TestUserGroups(t *testing.T) { diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 4d839ff63a..998315af4e 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -16,8 +16,8 @@ import ( "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) type cfConnector struct { diff --git a/connector/cf/cf_test.go b/connector/cf/cf_test.go index b9bf68dbeb..a1acb756e6 100644 --- a/connector/cf/cf_test.go +++ b/connector/cf/cf_test.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) func TestOpen(t *testing.T) { diff --git a/connector/gitea/gitea.go b/connector/gitea/gitea.go index 33cc3126e6..308550adee 100644 --- a/connector/gitea/gitea.go +++ b/connector/gitea/gitea.go @@ -14,8 +14,8 @@ import ( "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) // Config holds configuration options for gitea logins. diff --git a/connector/gitea/gitea_test.go b/connector/gitea/gitea_test.go index a71d79956e..c4576d0fd2 100644 --- a/connector/gitea/gitea_test.go +++ b/connector/gitea/gitea_test.go @@ -9,7 +9,7 @@ import ( "reflect" "testing" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) // tests that the email is used as their username when they have no username set diff --git a/connector/github/github.go b/connector/github/github.go index 02f2cae804..a1291af5de 100644 --- a/connector/github/github.go +++ b/connector/github/github.go @@ -19,9 +19,9 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/github" - "github.com/dexidp/dex/connector" - groups_pkg "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + groups_pkg "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" ) const ( diff --git a/connector/github/github_test.go b/connector/github/github_test.go index 76d7463cf6..b9d10d7d27 100644 --- a/connector/github/github_test.go +++ b/connector/github/github_test.go @@ -12,7 +12,7 @@ import ( "strings" "testing" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) type testResponse struct { diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go index e40601402d..e70f1eaded 100644 --- a/connector/gitlab/gitlab.go +++ b/connector/gitlab/gitlab.go @@ -12,9 +12,9 @@ import ( "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" ) const ( diff --git a/connector/gitlab/gitlab_test.go b/connector/gitlab/gitlab_test.go index 23cf9aac2d..266e6dffce 100644 --- a/connector/gitlab/gitlab_test.go +++ b/connector/gitlab/gitlab_test.go @@ -10,7 +10,7 @@ import ( "reflect" "testing" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) func TestUserGroups(t *testing.T) { diff --git a/connector/google/google.go b/connector/google/google.go index eccb1fc7d7..747274dfdb 100644 --- a/connector/google/google.go +++ b/connector/google/google.go @@ -15,9 +15,9 @@ import ( admin "google.golang.org/api/admin/directory/v1" "google.golang.org/api/option" - "github.com/dexidp/dex/connector" - pkg_groups "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + pkg_groups "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" ) const ( diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go index d817bd9454..7b87de2e47 100644 --- a/connector/keystone/keystone.go +++ b/connector/keystone/keystone.go @@ -9,8 +9,8 @@ import ( "io/ioutil" "net/http" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) type conn struct { diff --git a/connector/keystone/keystone_test.go b/connector/keystone/keystone_test.go index b138012426..a14728bbb4 100644 --- a/connector/keystone/keystone_test.go +++ b/connector/keystone/keystone_test.go @@ -12,7 +12,7 @@ import ( "strings" "testing" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) const ( diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 2325b25ace..1733e1c88b 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -12,8 +12,8 @@ import ( "gopkg.in/ldap.v2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) // Config holds the configuration parameters for the LDAP connector. The LDAP diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go index 9ae4567431..9bbe1ac3a5 100644 --- a/connector/ldap/ldap_test.go +++ b/connector/ldap/ldap_test.go @@ -10,7 +10,7 @@ import ( "github.com/kylelemons/godebug/pretty" "github.com/sirupsen/logrus" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) // connectionMethod indicates how the test should connect to the LDAP server. diff --git a/connector/linkedin/linkedin.go b/connector/linkedin/linkedin.go index 1c8312c11e..6c2821af00 100644 --- a/connector/linkedin/linkedin.go +++ b/connector/linkedin/linkedin.go @@ -11,8 +11,8 @@ import ( "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) const ( diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go index 3a3cf3b5cc..87b6cfdc4a 100644 --- a/connector/microsoft/microsoft.go +++ b/connector/microsoft/microsoft.go @@ -15,9 +15,9 @@ import ( "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - groups_pkg "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + groups_pkg "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" ) // GroupNameFormat represents the format of the group identifier diff --git a/connector/microsoft/microsoft_test.go b/connector/microsoft/microsoft_test.go index 3fba9c2ae2..0cd6fa5028 100644 --- a/connector/microsoft/microsoft_test.go +++ b/connector/microsoft/microsoft_test.go @@ -9,7 +9,7 @@ import ( "reflect" "testing" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) type testResponse struct { diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go index 5089914ca1..98dc513063 100644 --- a/connector/mock/connectortest.go +++ b/connector/mock/connectortest.go @@ -8,8 +8,8 @@ import ( "net/http" "net/url" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) // NewCallbackConnector returns a mock connector which requires no user interaction. It always returns diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go index c709531e2a..ea91a8469f 100644 --- a/connector/oauth/oauth.go +++ b/connector/oauth/oauth.go @@ -16,8 +16,8 @@ import ( "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) type oauthConnector struct { diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go index b8074aa452..077dcc9987 100644 --- a/connector/oauth/oauth_test.go +++ b/connector/oauth/oauth_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/assert" jose "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) func TestOpen(t *testing.T) { diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index 8281d60a53..0145a537f5 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -18,8 +18,8 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/log" ) // Config holds configuration options for OpenID Connect logins. diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go index ae92f70caa..342a46418c 100644 --- a/connector/oidc/oidc_test.go +++ b/connector/oidc/oidc_test.go @@ -19,7 +19,7 @@ import ( "github.com/sirupsen/logrus" "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) func TestKnownBrokenAuthHeaderProvider(t *testing.T) { diff --git a/connector/openshift/openshift.go b/connector/openshift/openshift.go index f06e8f8045..cfd43481d3 100644 --- a/connector/openshift/openshift.go +++ b/connector/openshift/openshift.go @@ -14,10 +14,10 @@ import ( "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage/kubernetes/k8sapi" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage/kubernetes/k8sapi" ) // Config holds configuration options for OpenShift login diff --git a/connector/openshift/openshift_test.go b/connector/openshift/openshift_test.go index 90f1686c40..a430f0835c 100644 --- a/connector/openshift/openshift_test.go +++ b/connector/openshift/openshift_test.go @@ -13,8 +13,8 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/oauth2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/storage/kubernetes/k8sapi" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/storage/kubernetes/k8sapi" ) func TestOpen(t *testing.T) { diff --git a/connector/saml/saml.go b/connector/saml/saml.go index 0d52b13116..a809bc2779 100644 --- a/connector/saml/saml.go +++ b/connector/saml/saml.go @@ -19,9 +19,9 @@ import ( dsig "github.com/russellhaering/goxmldsig" "github.com/russellhaering/goxmldsig/etreeutils" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/groups" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/pkg/groups" + "github.com/concourse/dex/pkg/log" ) // nolint diff --git a/connector/saml/saml_test.go b/connector/saml/saml_test.go index 67d7efb140..4a721fd42f 100644 --- a/connector/saml/saml_test.go +++ b/connector/saml/saml_test.go @@ -14,7 +14,7 @@ import ( dsig "github.com/russellhaering/goxmldsig" "github.com/sirupsen/logrus" - "github.com/dexidp/dex/connector" + "github.com/concourse/dex/connector" ) // responseTest maps a SAML 2.0 response object to a set of expected values. diff --git a/examples/go.mod b/examples/go.mod index fc1405d615..f2748f1b3b 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -1,4 +1,4 @@ -module github.com/dexidp/dex/examples +module github.com/concourse/dex/examples go 1.14 diff --git a/go.mod b/go.mod index 6291284cca..7ee8e3aeb6 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/dexidp/dex +module github.com/concourse/dex go 1.16 diff --git a/pkg/groups/groups_test.go b/pkg/groups/groups_test.go index 0be62fb430..2ff38a2df8 100644 --- a/pkg/groups/groups_test.go +++ b/pkg/groups/groups_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/dexidp/dex/pkg/groups" + "github.com/concourse/dex/pkg/groups" ) func TestFilter(t *testing.T) { diff --git a/server/api.go b/server/api.go index 5560c3bccb..8d07960e38 100644 --- a/server/api.go +++ b/server/api.go @@ -8,10 +8,10 @@ import ( "golang.org/x/crypto/bcrypt" "github.com/dexidp/dex/api/v2" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/server/internal" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/version" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/server/internal" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/version" ) // apiVersion increases every time a new call is added to the API. Clients should use this info diff --git a/server/api_test.go b/server/api_test.go index e7725063a5..1208cdfa0f 100644 --- a/server/api_test.go +++ b/server/api_test.go @@ -11,10 +11,10 @@ import ( "google.golang.org/grpc" "github.com/dexidp/dex/api/v2" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/server/internal" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/memory" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/server/internal" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/memory" ) // apiClient is a test gRPC client. When constructed, it runs a server in diff --git a/server/deviceflowhandlers.go b/server/deviceflowhandlers.go index a73dafe8ee..4e887b8238 100644 --- a/server/deviceflowhandlers.go +++ b/server/deviceflowhandlers.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/storage" ) type deviceCodeResponse struct { diff --git a/server/deviceflowhandlers_test.go b/server/deviceflowhandlers_test.go index 3898d4fc8e..4fd59aa2bd 100644 --- a/server/deviceflowhandlers_test.go +++ b/server/deviceflowhandlers_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/storage" ) func TestDeviceVerificationURI(t *testing.T) { diff --git a/server/handlers.go b/server/handlers.go index 92127ea5ab..4a24e91e24 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -20,9 +20,9 @@ import ( "golang.org/x/crypto/bcrypt" jose "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/server/internal" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/server/internal" + "github.com/concourse/dex/storage" ) const ( diff --git a/server/handlers_test.go b/server/handlers_test.go index d195af64b4..a622d54926 100644 --- a/server/handlers_test.go +++ b/server/handlers_test.go @@ -18,8 +18,8 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/oauth2" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/memory" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/memory" ) func TestHandleHealth(t *testing.T) { diff --git a/server/oauth2.go b/server/oauth2.go index 992a063b18..0f88d32f8d 100644 --- a/server/oauth2.go +++ b/server/oauth2.go @@ -22,9 +22,9 @@ import ( jose "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/server/internal" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/server/internal" + "github.com/concourse/dex/storage" ) // TODO(ericchiang): clean this file up and figure out more idiomatic error handling. diff --git a/server/oauth2_test.go b/server/oauth2_test.go index 518e22ee86..d25575f2fc 100644 --- a/server/oauth2_test.go +++ b/server/oauth2_test.go @@ -13,8 +13,8 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/memory" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/memory" ) func TestParseAuthorizationRequest(t *testing.T) { diff --git a/server/rotation.go b/server/rotation.go index b7dd8116a8..6eaa4a4333 100644 --- a/server/rotation.go +++ b/server/rotation.go @@ -12,8 +12,8 @@ import ( "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" ) var errAlreadyRotated = errors.New("keys already rotated by another server instance") diff --git a/server/rotation_test.go b/server/rotation_test.go index 6f9b2ecb3f..6b8a468b7c 100644 --- a/server/rotation_test.go +++ b/server/rotation_test.go @@ -8,8 +8,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/memory" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/memory" ) func signingKeyID(t *testing.T, s storage.Storage) string { diff --git a/server/server.go b/server/server.go index 21b757080b..6698db5b9c 100644 --- a/server/server.go +++ b/server/server.go @@ -23,26 +23,26 @@ import ( "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/bcrypt" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/connector/atlassiancrowd" - "github.com/dexidp/dex/connector/authproxy" - "github.com/dexidp/dex/connector/bitbucketcloud" - "github.com/dexidp/dex/connector/cf" - "github.com/dexidp/dex/connector/gitea" - "github.com/dexidp/dex/connector/github" - "github.com/dexidp/dex/connector/gitlab" - "github.com/dexidp/dex/connector/google" - "github.com/dexidp/dex/connector/keystone" - "github.com/dexidp/dex/connector/ldap" - "github.com/dexidp/dex/connector/linkedin" - "github.com/dexidp/dex/connector/microsoft" - "github.com/dexidp/dex/connector/mock" - "github.com/dexidp/dex/connector/oauth" - "github.com/dexidp/dex/connector/oidc" - "github.com/dexidp/dex/connector/openshift" - "github.com/dexidp/dex/connector/saml" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/connector/atlassiancrowd" + "github.com/concourse/dex/connector/authproxy" + "github.com/concourse/dex/connector/bitbucketcloud" + "github.com/concourse/dex/connector/cf" + "github.com/concourse/dex/connector/gitea" + "github.com/concourse/dex/connector/github" + "github.com/concourse/dex/connector/gitlab" + "github.com/concourse/dex/connector/google" + "github.com/concourse/dex/connector/keystone" + "github.com/concourse/dex/connector/ldap" + "github.com/concourse/dex/connector/linkedin" + "github.com/concourse/dex/connector/microsoft" + "github.com/concourse/dex/connector/mock" + "github.com/concourse/dex/connector/oauth" + "github.com/concourse/dex/connector/oidc" + "github.com/concourse/dex/connector/openshift" + "github.com/concourse/dex/connector/saml" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" ) // LocalConnector is the local passwordDB connector which is an internal diff --git a/server/server_test.go b/server/server_test.go index cbb298e58a..70af6ab272 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -31,10 +31,10 @@ import ( "golang.org/x/oauth2" jose "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/connector/mock" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/memory" + "github.com/concourse/dex/connector" + "github.com/concourse/dex/connector/mock" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/memory" ) func mustLoad(s string) *rsa.PrivateKey { diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go index 3f5e2aa191..15a180fc66 100644 --- a/storage/conformance/conformance.go +++ b/storage/conformance/conformance.go @@ -11,7 +11,7 @@ import ( "golang.org/x/crypto/bcrypt" jose "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/storage" ) // ensure that values being tested on never expire. diff --git a/storage/conformance/transactions.go b/storage/conformance/transactions.go index 2fc6755b09..0e5e22be9b 100644 --- a/storage/conformance/transactions.go +++ b/storage/conformance/transactions.go @@ -6,7 +6,7 @@ import ( "golang.org/x/crypto/bcrypt" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/storage" ) // RunTransactionTests runs a test suite aimed a verifying the transaction diff --git a/storage/etcd/config.go b/storage/etcd/config.go index 14607316d2..79427f53e2 100644 --- a/storage/etcd/config.go +++ b/storage/etcd/config.go @@ -7,8 +7,8 @@ import ( "go.etcd.io/etcd/clientv3/namespace" "go.etcd.io/etcd/pkg/transport" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" ) var defaultDialTimeout = 2 * time.Second diff --git a/storage/etcd/etcd.go b/storage/etcd/etcd.go index 97aced45a2..585e85f9fc 100644 --- a/storage/etcd/etcd.go +++ b/storage/etcd/etcd.go @@ -9,8 +9,8 @@ import ( "go.etcd.io/etcd/clientv3" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" ) const ( diff --git a/storage/etcd/etcd_test.go b/storage/etcd/etcd_test.go index 122d7daeac..f404518640 100644 --- a/storage/etcd/etcd_test.go +++ b/storage/etcd/etcd_test.go @@ -12,8 +12,8 @@ import ( "github.com/sirupsen/logrus" "go.etcd.io/etcd/clientv3" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/conformance" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/conformance" ) func withTimeout(t time.Duration, f func()) { diff --git a/storage/etcd/types.go b/storage/etcd/types.go index f2ffd9f702..ab3fba7b03 100644 --- a/storage/etcd/types.go +++ b/storage/etcd/types.go @@ -5,7 +5,7 @@ import ( jose "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/storage" ) // AuthCode is a mirrored struct from storage with JSON struct tags diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go index 593f1c0338..a6a4ded586 100644 --- a/storage/kubernetes/client.go +++ b/storage/kubernetes/client.go @@ -25,9 +25,9 @@ import ( "github.com/ghodss/yaml" "golang.org/x/net/http2" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/kubernetes/k8sapi" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/kubernetes/k8sapi" ) type client struct { diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go index b670244a0e..cec25cc4db 100644 --- a/storage/kubernetes/storage.go +++ b/storage/kubernetes/storage.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/kubernetes/k8sapi" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/kubernetes/k8sapi" ) const ( diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go index 42ba19a401..879696fd45 100644 --- a/storage/kubernetes/storage_test.go +++ b/storage/kubernetes/storage_test.go @@ -17,8 +17,8 @@ import ( "github.com/stretchr/testify/suite" "sigs.k8s.io/testing_frameworks/integration" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/conformance" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/conformance" ) const kubeconfigTemplate = `apiVersion: v1 diff --git a/storage/kubernetes/types.go b/storage/kubernetes/types.go index 07e2508401..d6e831050b 100644 --- a/storage/kubernetes/types.go +++ b/storage/kubernetes/types.go @@ -6,8 +6,8 @@ import ( jose "gopkg.in/square/go-jose.v2" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/kubernetes/k8sapi" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/kubernetes/k8sapi" ) var crdMeta = k8sapi.TypeMeta{ diff --git a/storage/memory/memory.go b/storage/memory/memory.go index 82264205e7..8eec2a1557 100644 --- a/storage/memory/memory.go +++ b/storage/memory/memory.go @@ -6,8 +6,8 @@ import ( "sync" "time" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" ) // New returns an in memory storage. diff --git a/storage/memory/memory_test.go b/storage/memory/memory_test.go index 84a8826ef2..9c020ee095 100644 --- a/storage/memory/memory_test.go +++ b/storage/memory/memory_test.go @@ -6,8 +6,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/conformance" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/conformance" ) func TestStorage(t *testing.T) { diff --git a/storage/memory/static_test.go b/storage/memory/static_test.go index 8513e0ee89..fdca32882d 100644 --- a/storage/memory/static_test.go +++ b/storage/memory/static_test.go @@ -8,7 +8,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/storage" ) func TestStaticClients(t *testing.T) { diff --git a/storage/sql/config.go b/storage/sql/config.go index 0ce0f117db..5d2e78257e 100644 --- a/storage/sql/config.go +++ b/storage/sql/config.go @@ -15,8 +15,8 @@ import ( "github.com/go-sql-driver/mysql" "github.com/lib/pq" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" ) const ( diff --git a/storage/sql/config_test.go b/storage/sql/config_test.go index 1178728c1a..1eca0194a8 100644 --- a/storage/sql/config_test.go +++ b/storage/sql/config_test.go @@ -10,9 +10,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" - "github.com/dexidp/dex/storage/conformance" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" + "github.com/concourse/dex/storage/conformance" ) func withTimeout(t time.Duration, f func()) { diff --git a/storage/sql/crud.go b/storage/sql/crud.go index 4451e5c567..d742fb1f20 100644 --- a/storage/sql/crud.go +++ b/storage/sql/crud.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/storage" ) // TODO(ericchiang): The update, insert, and select methods queries are all diff --git a/storage/sql/sql.go b/storage/sql/sql.go index 0a29216936..dad6a79ad4 100644 --- a/storage/sql/sql.go +++ b/storage/sql/sql.go @@ -10,7 +10,7 @@ import ( _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/pkg/log" ) // flavor represents a specific SQL implementation, and is used to translate query strings diff --git a/storage/sql/sqlite.go b/storage/sql/sqlite.go index faefd89ae3..235fcbc714 100644 --- a/storage/sql/sqlite.go +++ b/storage/sql/sqlite.go @@ -8,8 +8,8 @@ import ( sqlite3 "github.com/mattn/go-sqlite3" - "github.com/dexidp/dex/pkg/log" - "github.com/dexidp/dex/storage" + "github.com/concourse/dex/pkg/log" + "github.com/concourse/dex/storage" ) // SQLite3 options for creating an SQL db. diff --git a/storage/static.go b/storage/static.go index 806b61f9cd..c5a8acaf90 100644 --- a/storage/static.go +++ b/storage/static.go @@ -4,7 +4,7 @@ import ( "errors" "strings" - "github.com/dexidp/dex/pkg/log" + "github.com/concourse/dex/pkg/log" ) // Tests for this code are in the "memory" package, since this package doesn't From 73d726a297c41c8ad7c9004ee8c82a2ac4046d8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Mar 2021 07:49:09 +0000 Subject: [PATCH 30/30] build(deps): bump github.com/golang/protobuf from 1.3.2 to 1.5.1 Bumps [github.com/golang/protobuf](https://github.com/golang/protobuf) from 1.3.2 to 1.5.1. - [Release notes](https://github.com/golang/protobuf/releases) - [Commits](https://github.com/golang/protobuf/compare/v1.3.2...v1.5.1) Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7ee8e3aeb6..d3bf607d57 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-sql-driver/mysql v1.5.0 github.com/gogo/protobuf v1.3.1 // indirect - github.com/golang/protobuf v1.3.2 + github.com/golang/protobuf v1.5.1 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 diff --git a/go.sum b/go.sum index 3e624f53f1..f57d0cf658 100644 --- a/go.sum +++ b/go.sum @@ -96,16 +96,19 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1 h1:jAbXjIeW2ZSW2AwFxlGTDoc2CjI2XujLkV3ArsZFCvc= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -453,6 +456,9 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=