From 54fb570ffa9b81986279a668c6382f518036978b Mon Sep 17 00:00:00 2001 From: Maksim Nabokikh Date: Tue, 6 Aug 2024 07:57:51 +0200 Subject: [PATCH 01/22] Fix scheme for DialURL ldap connection (#3677) * Use scheme without :// suffix * Make test ldap server listen on custom ports to avoid stepping into go-ldap defaults Signed-off-by: m.nabokikh --- .github/workflows/ci.yaml | 4 ++-- connector/ldap/ldap.go | 6 +++--- docker-compose.test.yaml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8cf94a64f9..534edea15f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -121,8 +121,8 @@ jobs: DEX_ETCD_ENDPOINTS: http://localhost:${{ job.services.etcd.ports[2379] }} DEX_LDAP_HOST: localhost - DEX_LDAP_PORT: 389 - DEX_LDAP_TLS_PORT: 636 + DEX_LDAP_PORT: 3890 + DEX_LDAP_TLS_PORT: 6360 DEX_KEYSTONE_URL: http://localhost:${{ job.services.keystone.ports[5000] }} DEX_KEYSTONE_ADMIN_URL: http://localhost:${{ job.services.keystone.ports[35357] }} diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 897f30cff1..856949d240 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -322,10 +322,10 @@ func (c *ldapConnector) do(_ context.Context, f func(c *ldap.Conn) error) error switch { case c.InsecureNoSSL: - u := url.URL{Scheme: "ldap://", Host: c.Host} + u := url.URL{Scheme: "ldap", Host: c.Host} conn, err = ldap.DialURL(u.String()) case c.StartTLS: - u := url.URL{Scheme: "ldap://", Host: c.Host} + u := url.URL{Scheme: "ldap", Host: c.Host} conn, err = ldap.DialURL(u.String()) if err != nil { return fmt.Errorf("failed to connect: %v", err) @@ -334,7 +334,7 @@ func (c *ldapConnector) do(_ context.Context, f func(c *ldap.Conn) error) error return fmt.Errorf("start TLS failed: %v", err) } default: - u := url.URL{Scheme: "ldaps://", Host: c.Host} + u := url.URL{Scheme: "ldaps", Host: c.Host} conn, err = ldap.DialURL(u.String(), ldap.DialWithTLSConfig(c.tlsConfig)) } if err != nil { diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 46dfd84c4d..933ff80164 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -11,8 +11,8 @@ services: LDAP_TLS: "true" LDAP_TLS_VERIFY_CLIENT: try ports: - - 389:389 - - 636:636 + - 3890:389 + - 6360:636 volumes: - ./connector/ldap/testdata/certs:/container/service/slapd/assets/certs - ./connector/ldap/testdata/schema.ldif:/container/service/slapd/assets/config/bootstrap/ldif/99-schema.ldif From 43956db7fd75c488a82c70cf231f44287300a75d Mon Sep 17 00:00:00 2001 From: Maksim Nabokikh Date: Wed, 7 Aug 2024 19:31:01 +0200 Subject: [PATCH 02/22] Change workdir for gomplate (#3684) Workaround to run gomplate from a non-root directory in distroless images, because gomplate tries to access CWD on start. See: https://github.com/hairyhenderson/gomplate/pull/2202 Signed-off-by: m.nabokikh --- cmd/docker-entrypoint/main.go | 46 ++++++++++------- cmd/docker-entrypoint/main_test.go | 79 +++++++++++++----------------- 2 files changed, 62 insertions(+), 63 deletions(-) diff --git a/cmd/docker-entrypoint/main.go b/cmd/docker-entrypoint/main.go index b0f8e277f3..14d837e5ee 100644 --- a/cmd/docker-entrypoint/main.go +++ b/cmd/docker-entrypoint/main.go @@ -22,20 +22,13 @@ func main() { os.Exit(1) } - if err := run(args, realExec, realWhich); err != nil { + if err := run(args, realExec, realWhich, realGomplate); err != nil { fmt.Println("error:", err.Error()) os.Exit(1) } } -func realExec(fork bool, args ...string) error { - if fork { - if output, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil { - return fmt.Errorf("cannot fork/exec command %s: %w (output: %q)", args, err, string(output)) - } - return nil - } - +func realExec(args ...string) error { argv0, err := exec.LookPath(args[0]) if err != nil { return fmt.Errorf("cannot lookup path for command %s: %w", args[0], err) @@ -56,34 +49,49 @@ func realWhich(path string) string { return fullPath } -func run(args []string, execFunc func(bool, ...string) error, whichFunc func(string) string) error { +func realGomplate(path string) (string, error) { + tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*") + if err != nil { + return "", fmt.Errorf("cannot create temp file: %w", err) + } + + cmd := exec.Command("gomplate", "-f", path, "-o", tmpFile.Name()) + // TODO(nabokihms): Workaround to run gomplate from a non-root directory in distroless images + // gomplate tries to access CWD on start, see: https://github.com/hairyhenderson/gomplate/pull/2202 + cmd.Dir = "/etc/dex" + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("error executing gomplate: %w, (output: %q)", err, string(output)) + } + + return tmpFile.Name(), nil +} + +func run(args []string, execFunc func(...string) error, whichFunc func(string) string, gomplateFunc func(string) (string, error)) error { if args[0] != "dex" && args[0] != whichFunc("dex") { - return execFunc(false, args...) + return execFunc(args...) } if args[1] != "serve" { - return execFunc(false, args...) + return execFunc(args...) } newArgs := []string{} for _, tplCandidate := range args { if hasSuffixes(tplCandidate, ".tpl", ".tmpl", ".yaml") { - tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*") + fileName, err := gomplateFunc(tplCandidate) if err != nil { - return fmt.Errorf("cannot create temp file: %w", err) - } - - if err := execFunc(true, "gomplate", "-f", tplCandidate, "-o", tmpFile.Name()); err != nil { return err } - newArgs = append(newArgs, tmpFile.Name()) + newArgs = append(newArgs, fileName) } else { newArgs = append(newArgs, tplCandidate) } } - return execFunc(false, newArgs...) + return execFunc(newArgs...) } func hasSuffixes(s string, suffixes ...string) bool { diff --git a/cmd/docker-entrypoint/main_test.go b/cmd/docker-entrypoint/main_test.go index c8aef16979..49da3b5f02 100644 --- a/cmd/docker-entrypoint/main_test.go +++ b/cmd/docker-entrypoint/main_test.go @@ -6,7 +6,7 @@ import ( ) type execArgs struct { - fork bool + gomplate bool argPrefixes []string } @@ -16,98 +16,89 @@ func TestRun(t *testing.T) { args []string execReturns error whichReturns string - wantExecArgs []execArgs + wantExecArgs execArgs wantErr error }{ { name: "executable not dex", args: []string{"tuna", "fish"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"tuna", "fish"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"tuna", "fish"}}, }, { name: "executable is full path to dex", args: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}, whichReturns: "/usr/local/bin/dex", - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}}, }, { name: "command is not serve", args: []string{"dex", "marshmallow", "zelda"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}}, }, { name: "no templates", args: []string{"dex", "serve", "config.yaml.not-a-template"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}, }, { name: "no templates", args: []string{"dex", "serve", "config.yaml.not-a-template"}, - wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}}, + wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}, }, { - name: ".tpl template", - args: []string{"dex", "serve", "config.tpl"}, - wantExecArgs: []execArgs{ - {fork: true, argPrefixes: []string{"gomplate", "-f", "config.tpl", "-o", "/tmp/dex.config.yaml-"}}, - {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, - }, + name: ".tpl template", + args: []string{"dex", "serve", "config.tpl"}, + wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, }, { - name: ".tmpl template", - args: []string{"dex", "serve", "config.tmpl"}, - wantExecArgs: []execArgs{ - {fork: true, argPrefixes: []string{"gomplate", "-f", "config.tmpl", "-o", "/tmp/dex.config.yaml-"}}, - {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, - }, + name: ".tmpl template", + args: []string{"dex", "serve", "config.tmpl"}, + wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, }, { - name: ".yaml template", - args: []string{"dex", "serve", "some/path/config.yaml"}, - wantExecArgs: []execArgs{ - {fork: true, argPrefixes: []string{"gomplate", "-f", "some/path/config.yaml", "-o", "/tmp/dex.config.yaml-"}}, - {fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, - }, + name: ".yaml template", + args: []string{"dex", "serve", "some/path/config.yaml"}, + wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - var gotExecForks []bool - var gotExecArgs [][]string - fakeExec := func(fork bool, args ...string) error { - gotExecForks = append(gotExecForks, fork) - gotExecArgs = append(gotExecArgs, args) + var gotExecArgs []string + var runsGomplate bool + + fakeExec := func(args ...string) error { + gotExecArgs = append(args, gotExecArgs...) return test.execReturns } fakeWhich := func(_ string) string { return test.whichReturns } - gotErr := run(test.args, fakeExec, fakeWhich) + fakeGomplate := func(file string) (string, error) { + runsGomplate = true + return "/tmp/dex.config.yaml-", nil + } + + gotErr := run(test.args, fakeExec, fakeWhich, fakeGomplate) if (test.wantErr == nil) != (gotErr == nil) { t.Errorf("wanted error %s, got %s", test.wantErr, gotErr) } - if !execArgsMatch(test.wantExecArgs, gotExecForks, gotExecArgs) { - t.Errorf("wanted exec args %+v, got %+v %+v", test.wantExecArgs, gotExecForks, gotExecArgs) + + if !execArgsMatch(test.wantExecArgs, runsGomplate, gotExecArgs) { + t.Errorf("wanted exec args %+v (running gomplate: %+v), got %+v (running gomplate: %+v)", + test.wantExecArgs.argPrefixes, test.wantExecArgs.gomplate, gotExecArgs, runsGomplate) } }) } } -func execArgsMatch(wantExecArgs []execArgs, gotForks []bool, gotExecArgs [][]string) bool { - if len(wantExecArgs) != len(gotForks) { +func execArgsMatch(wantExecArgs execArgs, gomplate bool, gotExecArgs []string) bool { + if wantExecArgs.gomplate != gomplate { return false } - - for i := range wantExecArgs { - if wantExecArgs[i].fork != gotForks[i] { + for i := range wantExecArgs.argPrefixes { + if !strings.HasPrefix(gotExecArgs[i], wantExecArgs.argPrefixes[i]) { return false } - for j := range wantExecArgs[i].argPrefixes { - if !strings.HasPrefix(gotExecArgs[i][j], wantExecArgs[i].argPrefixes[j]) { - return false - } - } } - return true } From 8d1b597346c81152a86a323fda8ef0e9b4f02f5b Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Tue, 10 Apr 2018 09:47:59 -0400 Subject: [PATCH 03/22] 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 1cf71c5038..1c3f336c4f 100644 --- a/server/server.go +++ b/server/server.go @@ -33,6 +33,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" @@ -640,6 +641,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 c8d34e0354c3019fab16496ede1d7d62f2e4233a Mon Sep 17 00:00:00 2001 From: Josh Winters Date: Thu, 4 Oct 2018 15:07:26 -0400 Subject: [PATCH 04/22] 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 9f80f919b7724b28b4a6f0bc4d907581cba2bbbd Mon Sep 17 00:00:00 2001 From: Daniel Lavoie Date: Thu, 4 Apr 2019 18:26:59 -0400 Subject: [PATCH 05/22] 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 61d07a36292b7759ab6ef03bca3b023df071f317 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Thu, 7 Nov 2019 12:36:10 -0500 Subject: [PATCH 06/22] 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 a5ed574e6b83f7fbaf1eb8c4eb029ed9a34fcba8 Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Mon, 18 Nov 2019 16:38:33 -0500 Subject: [PATCH 07/22] 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 934f55a505b78969562e0cd335cf6f53c55292f7 Mon Sep 17 00:00:00 2001 From: Zoe Tian Date: Mon, 7 Oct 2019 17:16:00 -0400 Subject: [PATCH 08/22] 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 031e4e556f4ce14602b901580c7f14254801053b Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 4 Nov 2019 17:06:23 -0500 Subject: [PATCH 09/22] 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 39c229a0c5d12f87f236f8e1f5f391efe552d72a Mon Sep 17 00:00:00 2001 From: Joshua Winters Date: Mon, 25 Nov 2019 15:15:30 -0500 Subject: [PATCH 10/22] 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 abf3401924a099076dd5799f7703ee82c713733e Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Mon, 13 Jan 2020 13:19:53 -0500 Subject: [PATCH 11/22] 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 d7dc0ec08a337a1c0f1093b65f6536f73500a235 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Fri, 5 Mar 2021 12:40:56 -0500 Subject: [PATCH 12/22] 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 5e302869deacd98ec2f9869e5f1363eb78db2557 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Wed, 1 Dec 2021 10:37:56 -0500 Subject: [PATCH 13/22] fix sanity check errors Signed-off-by: Rui Yang --- connector/cf/cf.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/connector/cf/cf.go b/connector/cf/cf.go index 4d839ff63a..4cd04275ca 100644 --- a/connector/cf/cf.go +++ b/connector/cf/cf.go @@ -7,9 +7,9 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "net" "net/http" + "os" "sort" "strings" "time" @@ -149,7 +149,7 @@ func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, err tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify} for _, rootCA := range rootCAs { - rootCABytes, err := ioutil.ReadFile(rootCA) + rootCABytes, err := os.ReadFile(rootCA) if err != nil { return nil, fmt.Errorf("failed to read root-ca: %v", err) } @@ -376,9 +376,9 @@ func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (ident return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) } - spaces := append(developerSpaces, append(auditorSpaces, managerSpaces...)...) + developerSpaces = append(developerSpaces, append(auditorSpaces, managerSpaces...)...) - identity.Groups = getGroupsClaims(orgs, spaces) + identity.Groups = getGroupsClaims(orgs, developerSpaces) } if s.OfflineAccess { From 8ecabd0f0191e06290e38e41f7d07a7c23c991cd Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Tue, 4 Oct 2022 22:53:06 -0400 Subject: [PATCH 14/22] rename connector;make types private; Signed-off-by: Rui Yang --- .../cf.go => cloudfoundry/cloudfoundry.go} | 66 +++++++++---------- .../cloudfoundry_test.go} | 18 ++--- server/server.go | 4 +- 3 files changed, 44 insertions(+), 44 deletions(-) rename connector/{cf/cf.go => cloudfoundry/cloudfoundry.go} (85%) rename connector/{cf/cf_test.go => cloudfoundry/cloudfoundry_test.go} (94%) diff --git a/connector/cf/cf.go b/connector/cloudfoundry/cloudfoundry.go similarity index 85% rename from connector/cf/cf.go rename to connector/cloudfoundry/cloudfoundry.go index 4cd04275ca..8d3ab7f0ae 100644 --- a/connector/cf/cf.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -1,4 +1,4 @@ -package cf +package cloudfoundry import ( "context" @@ -20,7 +20,7 @@ import ( "github.com/dexidp/dex/pkg/log" ) -type cfConnector struct { +type cloudfoundryConnector struct { clientID string clientSecret string redirectURI string @@ -45,34 +45,34 @@ type Config struct { InsecureSkipVerify bool `json:"insecureSkipVerify"` } -type CCResponse struct { +type ccResponse struct { NextURL string `json:"next_url"` - Resources []Resource `json:"resources"` + Resources []resource `json:"resources"` TotalResults int `json:"total_results"` } -type Resource struct { - Metadata Metadata `json:"metadata"` - Entity Entity `json:"entity"` +type resource struct { + Metadata metadata `json:"metadata"` + Entity entity `json:"entity"` } -type Metadata struct { +type metadata struct { GUID string `json:"guid"` } -type Entity struct { +type entity struct { Name string `json:"name"` OrganizationGUID string `json:"organization_guid"` } -type Space struct { +type space struct { Name string GUID string OrgGUID string Role string } -type Org struct { +type org struct { Name string GUID string } @@ -80,7 +80,7 @@ type Org struct { func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error - cfConn := &cfConnector{ + cloudfoundryConn := &cloudfoundryConnector{ clientID: c.ClientID, clientSecret: c.ClientSecret, apiURL: c.APIURL, @@ -88,13 +88,13 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) logger: logger, } - cfConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) + cloudfoundryConn.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)) + apiResp, err := cloudfoundryConn.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 @@ -112,7 +112,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) json.NewDecoder(apiResp.Body).Decode(&apiResult) uaaURL := strings.TrimRight(apiResult["authorization_endpoint"].(string), "/") - uaaResp, err := cfConn.httpClient.Get(fmt.Sprintf("%s/.well-known/openid-configuration", uaaURL)) + uaaResp, err := cloudfoundryConn.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 @@ -134,11 +134,11 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) return nil, err } - cfConn.tokenURL, _ = uaaResult["token_endpoint"].(string) - cfConn.authorizationURL, _ = uaaResult["authorization_endpoint"].(string) - cfConn.userInfoURL, _ = uaaResult["userinfo_endpoint"].(string) + cloudfoundryConn.tokenURL, _ = uaaResult["token_endpoint"].(string) + cloudfoundryConn.authorizationURL, _ = uaaResult["authorization_endpoint"].(string) + cloudfoundryConn.userInfoURL, _ = uaaResult["userinfo_endpoint"].(string) - return cfConn, err + return cloudfoundryConn, err } func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) { @@ -175,7 +175,7 @@ func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, err }, nil } -func (c *cfConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) { +func (c *cloudfoundryConnector) 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) } @@ -191,15 +191,15 @@ 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) { resources, err := fetchResources(baseURL, path, client) if err != nil { return nil, fmt.Errorf("failed to fetch resources: %v", err) } - spaces := make([]Space, len(resources)) + spaces := make([]space, len(resources)) for i, resource := range resources { - spaces[i] = Space{ + spaces[i] = space{ Name: resource.Entity.Name, GUID: resource.Metadata.GUID, OrgGUID: resource.Entity.OrganizationGUID, @@ -210,15 +210,15 @@ 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) { resources, err := fetchResources(baseURL, path, client) if err != nil { return nil, fmt.Errorf("failed to fetch resources: %v", err) } - orgs := make([]Org, len(resources)) + orgs := make([]org, len(resources)) for i, resource := range resources { - orgs[i] = Org{ + orgs[i] = org{ Name: resource.Entity.Name, GUID: resource.Metadata.GUID, } @@ -227,9 +227,9 @@ func fetchOrgs(baseURL, path string, client *http.Client) ([]Org, error) { 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 + resources []resource url string ) @@ -246,7 +246,7 @@ func fetchResources(baseURL, path string, client *http.Client) ([]Resource, erro return nil, fmt.Errorf("unsuccessful status code %d", resp.StatusCode) } - response := CCResponse{} + response := ccResponse{} err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return nil, fmt.Errorf("failed to parse spaces: %v", err) @@ -263,16 +263,16 @@ func fetchResources(baseURL, path string, client *http.Client) ([]Resource, erro return resources, nil } -func getGroupsClaims(orgs []Org, spaces []Space) []string { +func getGroupsClaims(orgs []org, spaces []space) []string { var ( orgMap = map[string]string{} - orgSpaces = map[string][]Space{} + orgSpaces = map[string][]space{} groupsClaims = map[string]bool{} ) for _, org := range orgs { orgMap[org.GUID] = org.Name - orgSpaces[org.Name] = []Space{} + orgSpaces[org.Name] = []space{} groupsClaims[org.GUID] = true groupsClaims[org.Name] = true } @@ -301,7 +301,7 @@ func getGroupsClaims(orgs []Org, spaces []Space) []string { return groups } -func (c *cfConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { +func (c *cloudfoundryConnector) 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/cloudfoundry/cloudfoundry_test.go similarity index 94% rename from connector/cf/cf_test.go rename to connector/cloudfoundry/cloudfoundry_test.go index b9bf68dbeb..73b521a917 100644 --- a/connector/cf/cf_test.go +++ b/connector/cloudfoundry/cloudfoundry_test.go @@ -1,4 +1,4 @@ -package cf +package cloudfoundry import ( "encoding/json" @@ -30,7 +30,7 @@ func TestHandleCallback(t *testing.T) { testServer := testSetup() defer testServer.Close() - cfConn := &cfConnector{ + cloudfoundryConn := &cloudfoundryConnector{ tokenURL: fmt.Sprintf("%s/token", testServer.URL), authorizationURL: fmt.Sprintf("%s/authorize", testServer.URL), userInfoURL: fmt.Sprintf("%s/userinfo", testServer.URL), @@ -45,7 +45,7 @@ func TestHandleCallback(t *testing.T) { expectEqual(t, err, nil) t.Run("CallbackWithGroupsScope", func(t *testing.T) { - identity, err := cfConn.HandleCallback(connector.Scopes{Groups: true}, req) + identity, err := cloudfoundryConn.HandleCallback(connector.Scopes{Groups: true}, req) expectEqual(t, err, nil) expectEqual(t, len(identity.Groups), 24) @@ -76,7 +76,7 @@ func TestHandleCallback(t *testing.T) { }) t.Run("CallbackWithoutGroupsScope", func(t *testing.T) { - identity, err := cfConn.HandleCallback(connector.Scopes{}, req) + identity, err := cloudfoundryConn.HandleCallback(connector.Scopes{}, req) expectEqual(t, err, nil) expectEqual(t, identity.UserID, "12345") @@ -84,7 +84,7 @@ func TestHandleCallback(t *testing.T) { }) t.Run("CallbackWithOfflineAccessScope", func(t *testing.T) { - identity, err := cfConn.HandleCallback(connector.Scopes{OfflineAccess: true}, req) + identity, err := cloudfoundryConn.HandleCallback(connector.Scopes{OfflineAccess: true}, req) expectEqual(t, err, nil) expectNotEqual(t, len(identity.ConnectorData), 0) @@ -220,7 +220,7 @@ func testSetup() *httptest.Server { return httptest.NewServer(mux) } -func newConnector(t *testing.T, serverURL string) *cfConnector { +func newConnector(t *testing.T, serverURL string) *cloudfoundryConnector { callBackURL := fmt.Sprintf("%s/callback", serverURL) testConfig := Config{ @@ -238,12 +238,12 @@ func newConnector(t *testing.T, serverURL string) *cfConnector { t.Fatal(err) } - cfConn, ok := conn.(*cfConnector) + cloudfoundryConn, ok := conn.(*cloudfoundryConnector) if !ok { - t.Fatal(errors.New("it is not a cf conn")) + t.Fatal(errors.New("it is not a cloudfoundry conn")) } - return cfConn + return cloudfoundryConn } func expectEqual(t *testing.T, a interface{}, b interface{}) { diff --git a/server/server.go b/server/server.go index 1c3f336c4f..13db0d7b5d 100644 --- a/server/server.go +++ b/server/server.go @@ -33,7 +33,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/cloudfoundry" "github.com/dexidp/dex/connector/gitea" "github.com/dexidp/dex/connector/github" "github.com/dexidp/dex/connector/gitlab" @@ -641,7 +641,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) }, + "cloudfoundry": func() ConnectorConfig { return new(cloudfoundry.Config) }, // Keep around for backwards compatibility. "samlExperimental": func() ConnectorConfig { return new(saml.Config) }, } From a2099f492fb29b7df3aa5b40c357cd75e91faa79 Mon Sep 17 00:00:00 2001 From: Rui Yang Date: Tue, 4 Oct 2022 23:28:34 -0400 Subject: [PATCH 15/22] add cloudfoundry to connector list in readme Signed-off-by: Rui Yang --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2894dcdd46..6127d85bba 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ Dex implements the following connectors: | [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 | beta | | | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | | +| [Cloud Foundry](https://dexidp.io/docs/connectors/cloudfoundry/) | no | yes | no | alpha | This connector is community maintained by [Concourse](https://github.com/concourse) | Stable, beta, and alpha are defined as: From fda2d8cb567f82c7976db25e0bce836febbef405 Mon Sep 17 00:00:00 2001 From: Kump3r Date: Wed, 2 Oct 2024 15:40:33 +0300 Subject: [PATCH 16/22] Switching to CloudFoundry v3 API Signed-off-by: Kump3r --- connector/cloudfoundry/cloudfoundry.go | 8 ++++---- connector/cloudfoundry/cloudfoundry_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/connector/cloudfoundry/cloudfoundry.go b/connector/cloudfoundry/cloudfoundry.go index 8d3ab7f0ae..3eeaae9881 100644 --- a/connector/cloudfoundry/cloudfoundry.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -349,10 +349,10 @@ func (c *cloudfoundryConnector) HandleCallback(s connector.Scopes, r *http.Reque identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) 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) + devPath = fmt.Sprintf("/v3/users/%s/spaces", identity.UserID) + auditorPath = fmt.Sprintf("/v3/users/%s/audited_spaces", identity.UserID) + managerPath = fmt.Sprintf("/v3/users/%s/managed_spaces", identity.UserID) + orgsPath = fmt.Sprintf("/v3/users/%s/organizations", identity.UserID) ) if s.Groups { diff --git a/connector/cloudfoundry/cloudfoundry_test.go b/connector/cloudfoundry/cloudfoundry_test.go index 73b521a917..b7c2e0ba46 100644 --- a/connector/cloudfoundry/cloudfoundry_test.go +++ b/connector/cloudfoundry/cloudfoundry_test.go @@ -109,7 +109,7 @@ 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("/v3/users/12345/%s?order-direction=asc&page=2&results-per-page=50", spaceAPIEndpoint) result = map[string]interface{}{ "next_url": nextURL, "resources": []map[string]interface{}{ @@ -139,7 +139,7 @@ func testOrgHandler(reqURL string) (result map[string]interface{}) { } } else { result = map[string]interface{}{ - "next_url": "/v2/users/12345/organizations?order-direction=asc&page=2&results-per-page=50", + "next_url": "/v3/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"}, @@ -165,7 +165,7 @@ func testSetup() *httptest.Server { }) }) - mux.HandleFunc("/v2/info", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/v3/info", func(w http.ResponseWriter, r *http.Request) { url := fmt.Sprintf("http://%s", r.Host) json.NewEncoder(w).Encode(map[string]string{ @@ -194,7 +194,7 @@ func testSetup() *httptest.Server { }) }) - mux.HandleFunc("/v2/users/", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/v3/users/", func(w http.ResponseWriter, r *http.Request) { var result map[string]interface{} reqURL := r.URL.String() From c67eb796115f1e051b920a067a5995f23f945dd3 Mon Sep 17 00:00:00 2001 From: Kump3r Date: Fri, 3 Jan 2025 11:25:51 +0200 Subject: [PATCH 17/22] Add missing endpoint Signed-off-by: Kump3r --- connector/cloudfoundry/cloudfoundry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector/cloudfoundry/cloudfoundry.go b/connector/cloudfoundry/cloudfoundry.go index 3eeaae9881..acfb77fdb3 100644 --- a/connector/cloudfoundry/cloudfoundry.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -94,7 +94,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) } apiURL := strings.TrimRight(c.APIURL, "/") - apiResp, err := cloudfoundryConn.httpClient.Get(fmt.Sprintf("%s/v2/info", apiURL)) + apiResp, err := cloudfoundryConn.httpClient.Get(fmt.Sprintf("%s/v3/info", apiURL)) if err != nil { logger.Errorf("failed-to-send-request-to-cloud-controller-api", err) return nil, err From ed2e674dc2e606eea78835f2e847dac7068c32b5 Mon Sep 17 00:00:00 2001 From: Kump3r Date: Tue, 7 Jan 2025 12:28:51 +0200 Subject: [PATCH 18/22] Refactor CloudFoundry API request handling to use updated response structure Signed-off-by: Kump3r --- connector/cloudfoundry/cloudfoundry.go | 13 ++++++++++--- connector/cloudfoundry/cloudfoundry_test.go | 10 +++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/connector/cloudfoundry/cloudfoundry.go b/connector/cloudfoundry/cloudfoundry.go index acfb77fdb3..32ec6a9157 100644 --- a/connector/cloudfoundry/cloudfoundry.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -94,7 +94,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) } apiURL := strings.TrimRight(c.APIURL, "/") - apiResp, err := cloudfoundryConn.httpClient.Get(fmt.Sprintf("%s/v3/info", apiURL)) + apiResp, err := cloudfoundryConn.httpClient.Get(apiURL) if err != nil { logger.Errorf("failed-to-send-request-to-cloud-controller-api", err) return nil, err @@ -108,10 +108,17 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) return nil, err } - var apiResult map[string]interface{} + var apiResult struct { + Links struct { + Login struct { + Href string `json:"href"` + } `json:"login"` + } `json:"links"` + } + json.NewDecoder(apiResp.Body).Decode(&apiResult) - uaaURL := strings.TrimRight(apiResult["authorization_endpoint"].(string), "/") + uaaURL := strings.TrimRight(apiResult.Links.Login.Href, "/") uaaResp, err := cloudfoundryConn.httpClient.Get(fmt.Sprintf("%s/.well-known/openid-configuration", uaaURL)) if err != nil { logger.Errorf("failed-to-send-request-to-uaa-api", err) diff --git a/connector/cloudfoundry/cloudfoundry_test.go b/connector/cloudfoundry/cloudfoundry_test.go index b7c2e0ba46..e90f66a7c9 100644 --- a/connector/cloudfoundry/cloudfoundry_test.go +++ b/connector/cloudfoundry/cloudfoundry_test.go @@ -165,11 +165,15 @@ func testSetup() *httptest.Server { }) }) - mux.HandleFunc("/v3/info", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { url := fmt.Sprintf("http://%s", r.Host) - json.NewEncoder(w).Encode(map[string]string{ - "authorization_endpoint": url, + json.NewEncoder(w).Encode(map[string]interface{}{ + "links": map[string]interface{}{ + "login": map[string]string{ + "href": url, + }, + }, }) }) From b4fbac46f8d151a096918255f33be2258b068932 Mon Sep 17 00:00:00 2001 From: Kump3r Date: Thu, 9 Jan 2025 15:35:04 +0200 Subject: [PATCH 19/22] Use simple structures for the apiResult Signed-off-by: Kump3r --- connector/cloudfoundry/cloudfoundry.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/connector/cloudfoundry/cloudfoundry.go b/connector/cloudfoundry/cloudfoundry.go index 32ec6a9157..1aad39ce95 100644 --- a/connector/cloudfoundry/cloudfoundry.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -77,6 +77,18 @@ type org struct { GUID string } +type infoResp struct { + Links links `json:"links"` +} + +type links struct { + Login login `json:"login"` +} + +type login struct { + Href string `json:"href"` +} + func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { var err error @@ -108,13 +120,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) return nil, err } - var apiResult struct { - Links struct { - Login struct { - Href string `json:"href"` - } `json:"login"` - } `json:"links"` - } + var apiResult infoResp json.NewDecoder(apiResp.Body).Decode(&apiResult) From 716709b27959b8d56cb71d13462a509bd7081f3b Mon Sep 17 00:00:00 2001 From: IvanChalukov Date: Thu, 9 Jan 2025 16:58:54 +0200 Subject: [PATCH 20/22] Switching to CF API V3 endpoints Signed-off-by: IvanChalukov --- connector/cloudfoundry/cloudfoundry.go | 119 ++++++++++++++----------- 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/connector/cloudfoundry/cloudfoundry.go b/connector/cloudfoundry/cloudfoundry.go index 1aad39ce95..3ba88df5ee 100644 --- a/connector/cloudfoundry/cloudfoundry.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -46,25 +46,43 @@ type Config struct { } type ccResponse struct { - NextURL string `json:"next_url"` - Resources []resource `json:"resources"` - TotalResults int `json:"total_results"` + Pagination pagination `json:"pagination"` + Resources []resource `json:"resources"` } -type resource struct { - Metadata metadata `json:"metadata"` - Entity entity `json:"entity"` +type pagination struct { + Next href `json:"next"` +} + +type href struct { + Href string `json:"href"` } -type metadata struct { +type resource struct { GUID string `json:"guid"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Relationships relationships `json:"relationships"` } -type entity struct { - Name string `json:"name"` - OrganizationGUID string `json:"organization_guid"` +type relationships struct { + Organization relOrganization `json:"organization"` + Space relSpace `json:"space"` } +type relOrganization struct { + Data data `json:"data"` +} + +type relSpace struct { + Data data `json:"data"` +} + +type data struct { + GUID string `json:"guid"` +} + + type space struct { Name string GUID string @@ -204,40 +222,39 @@ func (c *cloudfoundryConnector) LoginURL(scopes connector.Scopes, callbackURL, s return oauth2Config.AuthCodeURL(state), nil } -func fetchRoleSpaces(baseURL, path, role string, client *http.Client) ([]space, error) { - resources, err := fetchResources(baseURL, path, client) - if err != nil { - return nil, fmt.Errorf("failed to fetch resources: %v", err) - } +func filterUserOrgsSpaces(userOrgsSpaces []resource, orgs []resource, spaces []resource) ([]org, []space) { + var filteredOrgs []org + var filteredSpaces []space + + orgMap := make(map[string]org) + spaceMap := make(map[string]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, + for _, org_resource := range orgs { + orgMap[org_resource.GUID] = org{ + Name: org_resource.Name, + GUID: org_resource.GUID, } } - return spaces, nil -} - -func fetchOrgs(baseURL, path string, client *http.Client) ([]org, error) { - resources, err := fetchResources(baseURL, path, client) - if err != nil { - return nil, fmt.Errorf("failed to fetch resources: %v", err) + for _, space_resource := range spaces { + spaceMap[space_resource.GUID] = space{ + Name: space_resource.Name, + GUID: space_resource.GUID, + OrgGUID: space_resource.Relationships.Organization.Data.GUID, + } } - orgs := make([]org, len(resources)) - for i, resource := range resources { - orgs[i] = org{ - Name: resource.Entity.Name, - GUID: resource.Metadata.GUID, + for _, userOrgSpace := range userOrgsSpaces { + if space, ok := spaceMap[userOrgSpace.Relationships.Space.Data.GUID]; ok { + space.Role = strings.TrimPrefix(userOrgSpace.Type, "space_") + filteredSpaces = append(filteredSpaces, space) + } + if org, ok := orgMap[userOrgSpace.Relationships.Organization.Data.GUID]; ok { + filteredOrgs = append(filteredOrgs, org) } } - return orgs, nil + return filteredOrgs, filteredSpaces } func fetchResources(baseURL, path string, client *http.Client) ([]resource, error) { @@ -262,12 +279,12 @@ func fetchResources(baseURL, path string, client *http.Client) ([]resource, erro response := ccResponse{} err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { - return nil, fmt.Errorf("failed to parse spaces: %v", err) + return nil, fmt.Errorf("failed to parse response: %v", err) } resources = append(resources, response.Resources...) - path = response.NextURL + path = strings.TrimPrefix(response.Pagination.Next.Href, baseURL) if path == "" { break } @@ -362,36 +379,30 @@ func (c *cloudfoundryConnector) HandleCallback(s connector.Scopes, r *http.Reque identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) var ( - devPath = fmt.Sprintf("/v3/users/%s/spaces", identity.UserID) - auditorPath = fmt.Sprintf("/v3/users/%s/audited_spaces", identity.UserID) - managerPath = fmt.Sprintf("/v3/users/%s/managed_spaces", identity.UserID) - orgsPath = fmt.Sprintf("/v3/users/%s/organizations", identity.UserID) + orgsPath = fmt.Sprintf("/v3/organizations") + spacesPath = fmt.Sprintf("/v3/spaces") + userOrgsSpacesPath = fmt.Sprintf("/v3/roles?user_guids=%s&types=space_developer,space_manager,space_auditor,organization_user", identity.UserID) ) if s.Groups { - orgs, err := fetchOrgs(c.apiURL, orgsPath, client) - if err != nil { - return identity, fmt.Errorf("failed to fetch organizaitons: %v", err) - } - - developerSpaces, err := fetchRoleSpaces(c.apiURL, devPath, "developer", client) + userOrgsSpaces, err := fetchResources(c.apiURL, userOrgsSpacesPath, client) if err != nil { - return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) + return identity, fmt.Errorf("failed to fetch user organizations: %v", err) } - auditorSpaces, err := fetchRoleSpaces(c.apiURL, auditorPath, "auditor", client) + orgs, err := fetchResources(c.apiURL, orgsPath, client) if err != nil { - return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) + return identity, fmt.Errorf("failed to fetch organizaitons: %v", err) } - managerSpaces, err := fetchRoleSpaces(c.apiURL, managerPath, "manager", client) + spaces, err := fetchResources(c.apiURL, spacesPath, client) if err != nil { - return identity, fmt.Errorf("failed to fetch spaces for developer roles: %v", err) + return identity, fmt.Errorf("failed to fetch spaces: %v", err) } - developerSpaces = append(developerSpaces, append(auditorSpaces, managerSpaces...)...) + developerOrgs, developerSpaces := filterUserOrgsSpaces(userOrgsSpaces, orgs, spaces) - identity.Groups = getGroupsClaims(orgs, developerSpaces) + identity.Groups = getGroupsClaims(developerOrgs, developerSpaces) } if s.OfflineAccess { From b94e636f005f2f7d1a9c2d441fbcff1a8481d527 Mon Sep 17 00:00:00 2001 From: Kump3r Date: Fri, 10 Jan 2025 16:20:58 +0200 Subject: [PATCH 21/22] Addapt cloudfoundry_test.go acceptance tests for CF API v3 Signed-off-by: Kump3r --- connector/cloudfoundry/cloudfoundry.go | 19 +- connector/cloudfoundry/cloudfoundry_test.go | 351 +++++++++++++++++--- 2 files changed, 322 insertions(+), 48 deletions(-) diff --git a/connector/cloudfoundry/cloudfoundry.go b/connector/cloudfoundry/cloudfoundry.go index 3ba88df5ee..472c14e9ad 100644 --- a/connector/cloudfoundry/cloudfoundry.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -47,27 +47,27 @@ type Config struct { type ccResponse struct { Pagination pagination `json:"pagination"` - Resources []resource `json:"resources"` + Resources []resource `json:"resources"` } type pagination struct { - Next href `json:"next"` + Next href `json:"next"` } type href struct { - Href string `json:"href"` + Href string `json:"href"` } type resource struct { - GUID string `json:"guid"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` + GUID string `json:"guid"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` Relationships relationships `json:"relationships"` } type relationships struct { Organization relOrganization `json:"organization"` - Space relSpace `json:"space"` + Space relSpace `json:"space"` } type relOrganization struct { @@ -82,7 +82,6 @@ type data struct { GUID string `json:"guid"` } - type space struct { Name string GUID string @@ -379,8 +378,8 @@ func (c *cloudfoundryConnector) HandleCallback(s connector.Scopes, r *http.Reque identity.EmailVerified, _ = userInfoResult["email_verified"].(bool) var ( - orgsPath = fmt.Sprintf("/v3/organizations") - spacesPath = fmt.Sprintf("/v3/spaces") + orgsPath = "/v3/organizations" + spacesPath = "/v3/spaces" userOrgsSpacesPath = fmt.Sprintf("/v3/roles?user_guids=%s&types=space_developer,space_manager,space_auditor,organization_user", identity.UserID) ) diff --git a/connector/cloudfoundry/cloudfoundry_test.go b/connector/cloudfoundry/cloudfoundry_test.go index e90f66a7c9..383ade22f6 100644 --- a/connector/cloudfoundry/cloudfoundry_test.go +++ b/connector/cloudfoundry/cloudfoundry_test.go @@ -97,25 +97,51 @@ 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 string) (result map[string]interface{}) { + if strings.Contains(reqURL, "spaces?page=2&per_page=50") { result = map[string]interface{}{ + "pagination": map[string]interface{}{ + "next": map[string]interface{}{ + "href": nil, + }, + }, "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"}, + "guid": "some-space-guid-2", + "name": "some-space-name-2", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-2", + }, + }, + "space": nil, + }, }, }, } } else { - nextURL := fmt.Sprintf("/v3/users/12345/%s?order-direction=asc&page=2&results-per-page=50", spaceAPIEndpoint) + nextURL := fmt.Sprintf("%s?page=2&per_page=50", reqURL) result = map[string]interface{}{ - "next_url": nextURL, + "pagination": map[string]interface{}{ + "next": map[string]interface{}{ + "href": 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"}, + "guid": "some-space-guid-1", + "name": "some-space-name-1", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-1", + }, + }, + "space": nil, + }, }, }, } @@ -124,30 +150,290 @@ func testSpaceHandler(reqURL, spaceAPIEndpoint string) (result map[string]interf } func testOrgHandler(reqURL string) (result map[string]interface{}) { - if strings.Contains(reqURL, "organizations?order-direction=asc&page=2&results-per-page=50") { + if strings.Contains(reqURL, "organizations?page=2&per_page=50") { + result = map[string]interface{}{ + "pagination": map[string]interface{}{ + "next": map[string]interface{}{ + "href": nil, + }, + }, + "resources": []map[string]interface{}{ + { + "guid": "some-org-guid-3", + "name": "some-org-name-3", + "relationships": map[string]interface{}{ + "user": nil, + "organization": nil, + "space": nil, + }, + }, + { + "guid": "some-org-guid-4", + "name": "some-org-name-4", + "relationships": map[string]interface{}{ + "user": nil, + "organization": nil, + "space": nil, + }, + }, + }, + } + } else { + nextURL := fmt.Sprintf("%s?page=2&per_page=50", reqURL) + result = map[string]interface{}{ + "pagination": map[string]interface{}{ + "next": map[string]interface{}{ + "href": nextURL, + }, + }, + "resources": []map[string]interface{}{ + { + "guid": "some-org-guid-1", + "name": "some-org-name-1", + "relationships": map[string]interface{}{ + "user": nil, + "organization": nil, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-1", + }, + }, + }, + }, + { + "guid": "some-org-guid-2", + "name": "some-org-name-2", + "relationships": map[string]interface{}{ + "user": nil, + "organization": nil, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-2", + }, + }, + }, + }, + }, + } + } + return result +} + +func testUserOrgsSpacesHandler(reqURL string) (result map[string]interface{}) { + if strings.Contains(reqURL, "page=2&per_page=50") { result = map[string]interface{}{ + "pagination": map[string]interface{}{ + "next": map[string]interface{}{ + "href": nil, + }, + }, "resources": []map[string]interface{}{ { - "metadata": map[string]string{"guid": "some-org-guid-3"}, - "entity": map[string]string{"name": "some-org-name-3"}, + "guid": "some-type-guid-3", + "type": "organization_user", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-3", + }, + }, + "space": nil, + }, + }, + { + "guid": "some-type-guid-4", + "type": "organization_user", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-4", + }, + }, + "space": nil, + }, + }, + { + "guid": "some-type-guid-1", + "type": "space_manager", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-1", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-1", + }, + }, + }, + }, + { + "guid": "some-type-guid-2", + "type": "space_developer", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-2", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-2", + }, + }, + }, }, { - "metadata": map[string]string{"guid": "some-org-guid-4"}, - "entity": map[string]string{"name": "some-org-name-4"}, + "guid": "some-type-guid-2", + "type": "space_auditor", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-2", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-2", + }, + }, + }, + }, + { + "guid": "some-type-guid-2", + "type": "space_manager", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-2", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-2", + }, + }, + }, }, }, } } else { + nextURL := fmt.Sprintf("%s?page=2&per_page=50", reqURL) result = map[string]interface{}{ - "next_url": "/v3/users/12345/organizations?order-direction=asc&page=2&results-per-page=50", + "pagination": map[string]interface{}{ + "next": map[string]interface{}{ + "href": nextURL, + }, + }, "resources": []map[string]interface{}{ { - "metadata": map[string]string{"guid": "some-org-guid-1"}, - "entity": map[string]string{"name": "some-org-name-1"}, + "guid": "some-type-guid-1", + "type": "space_developer", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-1", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-1", + }, + }, + }, }, { - "metadata": map[string]string{"guid": "some-org-guid-2"}, - "entity": map[string]string{"name": "some-org-name-2"}, + "guid": "some-type-guid-1", + "type": "space_auditor", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-1", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-1", + }, + }, + }, + }, + { + "guid": "some-type-guid-1", + "type": "space_manager", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-1", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-1", + }, + }, + }, + }, + { + "guid": "some-type-guid-2", + "type": "space_developer", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-2", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-2", + }, + }, + }, + }, + { + "guid": "some-type-guid-2", + "type": "space_auditor", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-2", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-2", + }, + }, + }, + }, + { + "guid": "some-type-guid-2", + "type": "space_manager", + "relationships": map[string]interface{}{ + "user": nil, + "organization": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-org-guid-2", + }, + }, + "space": map[string]interface{}{ + "data": map[string]interface{}{ + "guid": "some-space-guid-2", + }, + }, + }, }, }, } @@ -198,27 +484,16 @@ func testSetup() *httptest.Server { }) }) - mux.HandleFunc("/v3/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") - } - - if strings.Contains(reqURL, "/audited_spaces") { - result = testSpaceHandler(reqURL, "audited_spaces") - } - - if strings.Contains(reqURL, "/managed_spaces") { - result = testSpaceHandler(reqURL, "managed_spaces") - } + mux.HandleFunc("/v3/organizations", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(testOrgHandler(r.URL.String())) + }) - if strings.Contains(reqURL, "organizations") { - result = testOrgHandler(reqURL) - } + mux.HandleFunc("/v3/spaces", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(testSpaceHandler(r.URL.String())) + }) - json.NewEncoder(w).Encode(result) + mux.HandleFunc("/v3/roles", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(testUserOrgsSpacesHandler(r.URL.String())) }) return httptest.NewServer(mux) From c3165b181a64ba704b418bf4a544f6f10159d269 Mon Sep 17 00:00:00 2001 From: Taylor Silva Date: Thu, 16 Jan 2025 18:47:18 +0000 Subject: [PATCH 22/22] update cf connector to use slog instead Signed-off-by: Taylor Silva --- connector/cloudfoundry/cloudfoundry.go | 21 ++++++++------------- connector/cloudfoundry/cloudfoundry_test.go | 6 +++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/connector/cloudfoundry/cloudfoundry.go b/connector/cloudfoundry/cloudfoundry.go index 472c14e9ad..ff10bccd37 100644 --- a/connector/cloudfoundry/cloudfoundry.go +++ b/connector/cloudfoundry/cloudfoundry.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net" "net/http" "os" @@ -17,7 +18,6 @@ import ( "golang.org/x/oauth2" "github.com/dexidp/dex/connector" - "github.com/dexidp/dex/pkg/log" ) type cloudfoundryConnector struct { @@ -29,7 +29,7 @@ type cloudfoundryConnector struct { authorizationURL string userInfoURL string httpClient *http.Client - logger log.Logger + logger *slog.Logger } type connectorData struct { @@ -106,7 +106,7 @@ type login struct { Href string `json:"href"` } -func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) { +func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) { var err error cloudfoundryConn := &cloudfoundryConnector{ @@ -125,16 +125,14 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) apiURL := strings.TrimRight(c.APIURL, "/") apiResp, err := cloudfoundryConn.httpClient.Get(apiURL) if err != nil { - logger.Errorf("failed-to-send-request-to-cloud-controller-api", err) - return nil, err + return nil, fmt.Errorf("failed-to-send-request-to-cloud-controller-api: %w", err) } defer apiResp.Body.Close() if apiResp.StatusCode != http.StatusOK { err = fmt.Errorf("request failed with status %d", apiResp.StatusCode) - logger.Errorf("failed-get-info-response-from-api", err) - return nil, err + return nil, fmt.Errorf("failed-get-info-response-from-api: %w", err) } var apiResult infoResp @@ -144,14 +142,12 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) uaaURL := strings.TrimRight(apiResult.Links.Login.Href, "/") uaaResp, err := cloudfoundryConn.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 + return nil, fmt.Errorf("failed-to-send-request-to-uaa-api: %w", err) } if apiResp.StatusCode != http.StatusOK { 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 + return nil, fmt.Errorf("failed-to-get-well-known-config-response-from-api: %w", err) } defer uaaResp.Body.Close() @@ -160,8 +156,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) err = json.NewDecoder(uaaResp.Body).Decode(&uaaResult) if err != nil { - logger.Errorf("failed-to-decode-response-from-uaa-api", err) - return nil, err + return nil, fmt.Errorf("failed-to-decode-response-from-uaa-api: %w", err) } cloudfoundryConn.tokenURL, _ = uaaResult["token_endpoint"].(string) diff --git a/connector/cloudfoundry/cloudfoundry_test.go b/connector/cloudfoundry/cloudfoundry_test.go index 383ade22f6..d15b326104 100644 --- a/connector/cloudfoundry/cloudfoundry_test.go +++ b/connector/cloudfoundry/cloudfoundry_test.go @@ -4,14 +4,14 @@ import ( "encoding/json" "errors" "fmt" + "io" + "log/slog" "net/http" "net/http/httptest" "reflect" "strings" "testing" - "github.com/sirupsen/logrus" - "github.com/dexidp/dex/connector" ) @@ -510,7 +510,7 @@ func newConnector(t *testing.T, serverURL string) *cloudfoundryConnector { InsecureSkipVerify: true, } - log := logrus.New() + log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) conn, err := testConfig.Open("id", log) if err != nil {