mirror of https://github.com/dexidp/dex.git
4 changed files with 953 additions and 0 deletions
@ -0,0 +1,412 @@
|
||||
package cloudfoundry |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
"crypto/x509" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"log/slog" |
||||
"net" |
||||
"net/http" |
||||
"os" |
||||
"sort" |
||||
"strings" |
||||
"time" |
||||
|
||||
"golang.org/x/oauth2" |
||||
|
||||
"github.com/dexidp/dex/connector" |
||||
) |
||||
|
||||
type cloudfoundryConnector struct { |
||||
clientID string |
||||
clientSecret string |
||||
redirectURI string |
||||
apiURL string |
||||
tokenURL string |
||||
authorizationURL string |
||||
userInfoURL string |
||||
httpClient *http.Client |
||||
logger *slog.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 { |
||||
Pagination pagination `json:"pagination"` |
||||
Resources []resource `json:"resources"` |
||||
} |
||||
|
||||
type pagination struct { |
||||
Next href `json:"next"` |
||||
} |
||||
|
||||
type href struct { |
||||
Href string `json:"href"` |
||||
} |
||||
|
||||
type resource struct { |
||||
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"` |
||||
} |
||||
|
||||
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 |
||||
OrgGUID string |
||||
Role string |
||||
} |
||||
|
||||
type org struct { |
||||
Name string |
||||
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 *slog.Logger) (connector.Connector, error) { |
||||
var err error |
||||
|
||||
cloudfoundryConn := &cloudfoundryConnector{ |
||||
clientID: c.ClientID, |
||||
clientSecret: c.ClientSecret, |
||||
apiURL: c.APIURL, |
||||
redirectURI: c.RedirectURI, |
||||
logger: logger, |
||||
} |
||||
|
||||
cloudfoundryConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
apiURL := strings.TrimRight(c.APIURL, "/") |
||||
apiResp, err := cloudfoundryConn.httpClient.Get(apiURL) |
||||
if err != nil { |
||||
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) |
||||
return nil, fmt.Errorf("failed-get-info-response-from-api: %w", err) |
||||
} |
||||
|
||||
var apiResult infoResp |
||||
|
||||
json.NewDecoder(apiResp.Body).Decode(&apiResult) |
||||
|
||||
uaaURL := strings.TrimRight(apiResult.Links.Login.Href, "/") |
||||
uaaResp, err := cloudfoundryConn.httpClient.Get(fmt.Sprintf("%s/.well-known/openid-configuration", uaaURL)) |
||||
if err != nil { |
||||
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) |
||||
return nil, fmt.Errorf("failed-to-get-well-known-config-response-from-api: %w", err) |
||||
} |
||||
|
||||
defer uaaResp.Body.Close() |
||||
|
||||
var uaaResult map[string]interface{} |
||||
err = json.NewDecoder(uaaResp.Body).Decode(&uaaResult) |
||||
|
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed-to-decode-response-from-uaa-api: %w", err) |
||||
} |
||||
|
||||
cloudfoundryConn.tokenURL, _ = uaaResult["token_endpoint"].(string) |
||||
cloudfoundryConn.authorizationURL, _ = uaaResult["authorization_endpoint"].(string) |
||||
cloudfoundryConn.userInfoURL, _ = uaaResult["userinfo_endpoint"].(string) |
||||
|
||||
return cloudfoundryConn, 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 := os.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 *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) |
||||
} |
||||
|
||||
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 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) |
||||
|
||||
for _, org_resource := range orgs { |
||||
orgMap[org_resource.GUID] = org{ |
||||
Name: org_resource.Name, |
||||
GUID: org_resource.GUID, |
||||
} |
||||
} |
||||
|
||||
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, |
||||
} |
||||
} |
||||
|
||||
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 filteredOrgs, filteredSpaces |
||||
} |
||||
|
||||
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) |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
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 response: %v", err) |
||||
} |
||||
|
||||
resources = append(resources, response.Resources...) |
||||
|
||||
path = strings.TrimPrefix(response.Pagination.Next.Href, baseURL) |
||||
if path == "" { |
||||
break |
||||
} |
||||
} |
||||
|
||||
return resources, nil |
||||
} |
||||
|
||||
func getGroupsClaims(orgs []org, spaces []space) []string { |
||||
var ( |
||||
orgMap = map[string]string{} |
||||
orgSpaces = map[string][]space{} |
||||
groupsClaims = map[string]bool{} |
||||
) |
||||
|
||||
for _, org := range orgs { |
||||
orgMap[org.GUID] = org.Name |
||||
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) |
||||
groupsClaims[space.GUID] = true |
||||
groupsClaims[fmt.Sprintf("%s:%s", space.GUID, space.Role)] = true |
||||
} |
||||
|
||||
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 |
||||
} |
||||
} |
||||
|
||||
groups := make([]string, 0, len(groupsClaims)) |
||||
for group := range groupsClaims { |
||||
groups = append(groups, group) |
||||
} |
||||
|
||||
sort.Strings(groups) |
||||
|
||||
return groups |
||||
} |
||||
|
||||
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")) |
||||
} |
||||
|
||||
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) |
||||
|
||||
var ( |
||||
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) |
||||
) |
||||
|
||||
if s.Groups { |
||||
userOrgsSpaces, err := fetchResources(c.apiURL, userOrgsSpacesPath, client) |
||||
if err != nil { |
||||
return identity, fmt.Errorf("failed to fetch user organizations: %v", err) |
||||
} |
||||
|
||||
orgs, err := fetchResources(c.apiURL, orgsPath, client) |
||||
if err != nil { |
||||
return identity, fmt.Errorf("failed to fetch organizaitons: %v", err) |
||||
} |
||||
|
||||
spaces, err := fetchResources(c.apiURL, spacesPath, client) |
||||
if err != nil { |
||||
return identity, fmt.Errorf("failed to fetch spaces: %v", err) |
||||
} |
||||
|
||||
developerOrgs, developerSpaces := filterUserOrgsSpaces(userOrgsSpaces, orgs, spaces) |
||||
|
||||
identity.Groups = getGroupsClaims(developerOrgs, developerSpaces) |
||||
} |
||||
|
||||
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 |
||||
} |
||||
@ -0,0 +1,538 @@
|
||||
package cloudfoundry |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"log/slog" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"reflect" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/dexidp/dex/connector" |
||||
) |
||||
|
||||
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() |
||||
|
||||
cloudfoundryConn := &cloudfoundryConnector{ |
||||
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 := cloudfoundryConn.HandleCallback(connector.Scopes{Groups: true}, req) |
||||
expectEqual(t, err, nil) |
||||
|
||||
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-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) { |
||||
identity, err := cloudfoundryConn.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 := cloudfoundryConn.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 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{}{ |
||||
{ |
||||
"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("%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-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, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
return result |
||||
} |
||||
|
||||
func testOrgHandler(reqURL string) (result map[string]interface{}) { |
||||
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{}{ |
||||
{ |
||||
"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", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
"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{}{ |
||||
"pagination": map[string]interface{}{ |
||||
"next": map[string]interface{}{ |
||||
"href": nextURL, |
||||
}, |
||||
}, |
||||
"resources": []map[string]interface{}{ |
||||
{ |
||||
"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", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
"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", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
return result |
||||
} |
||||
|
||||
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("/", func(w http.ResponseWriter, r *http.Request) { |
||||
url := fmt.Sprintf("http://%s", r.Host) |
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{ |
||||
"links": map[string]interface{}{ |
||||
"login": map[string]string{ |
||||
"href": 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("/v3/organizations", func(w http.ResponseWriter, r *http.Request) { |
||||
json.NewEncoder(w).Encode(testOrgHandler(r.URL.String())) |
||||
}) |
||||
|
||||
mux.HandleFunc("/v3/spaces", func(w http.ResponseWriter, r *http.Request) { |
||||
json.NewEncoder(w).Encode(testSpaceHandler(r.URL.String())) |
||||
}) |
||||
|
||||
mux.HandleFunc("/v3/roles", func(w http.ResponseWriter, r *http.Request) { |
||||
json.NewEncoder(w).Encode(testUserOrgsSpacesHandler(r.URL.String())) |
||||
}) |
||||
|
||||
return httptest.NewServer(mux) |
||||
} |
||||
|
||||
func newConnector(t *testing.T, serverURL string) *cloudfoundryConnector { |
||||
callBackURL := fmt.Sprintf("%s/callback", serverURL) |
||||
|
||||
testConfig := Config{ |
||||
APIURL: serverURL, |
||||
ClientID: "test-client", |
||||
ClientSecret: "secret", |
||||
RedirectURI: callBackURL, |
||||
InsecureSkipVerify: true, |
||||
} |
||||
|
||||
log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) |
||||
|
||||
conn, err := testConfig.Open("id", log) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
cloudfoundryConn, ok := conn.(*cloudfoundryConnector) |
||||
if !ok { |
||||
t.Fatal(errors.New("it is not a cloudfoundry conn")) |
||||
} |
||||
|
||||
return cloudfoundryConn |
||||
} |
||||
|
||||
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) |
||||
} |
||||
} |
||||
Loading…
Reference in new issue