Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Github connector now returns a full group list when no org is specified #1184

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
14 changes: 8 additions & 6 deletions cmd/dex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
"github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"

"github.com/dexidp/dex/server"
"github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/etcd"
"github.com/dexidp/dex/storage/kubernetes"
"github.com/dexidp/dex/storage/memory"
"github.com/dexidp/dex/storage/sql"
"github.com/concourse/dex/server"
"github.com/concourse/dex/storage"
"github.com/concourse/dex/storage/etcd"
"github.com/concourse/dex/storage/kubernetes"
"github.com/concourse/dex/storage/memory"
"github.com/concourse/dex/storage/sql"
)

// Config is the config format for the main application.
Expand Down Expand Up @@ -94,6 +94,8 @@ type OAuth2 struct {
// If specified, do not prompt the user to approve client authorization. The
// act of logging in implies authorization.
SkipApprovalScreen bool `json:"skipApprovalScreen"`
// This is the connector that can be used for password grant
PasswordConnector string `json:"passwordConnector"`
}

// Web is the config format for the HTTP server.
Expand Down
8 changes: 4 additions & 4 deletions cmd/dex/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"github.com/ghodss/yaml"
"github.com/kylelemons/godebug/pretty"

"github.com/dexidp/dex/connector/mock"
"github.com/dexidp/dex/connector/oidc"
"github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/sql"
"github.com/concourse/dex/connector/mock"
"github.com/concourse/dex/connector/oidc"
"github.com/concourse/dex/storage"
"github.com/concourse/dex/storage/sql"
)

var _ = yaml.YAMLToJSON
Expand Down
10 changes: 7 additions & 3 deletions cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"

"github.com/dexidp/dex/api"
"github.com/dexidp/dex/server"
"github.com/dexidp/dex/storage"
"github.com/concourse/dex/api"
"github.com/concourse/dex/server"
"github.com/concourse/dex/storage"
)

func commandServe() *cobra.Command {
Expand Down Expand Up @@ -208,6 +208,9 @@ func serve(cmd *cobra.Command, args []string) error {
if c.OAuth2.SkipApprovalScreen {
logger.Infof("config skipping approval screen")
}
if c.OAuth2.PasswordConnector != "" {
logger.Infof("config using password grant connector: %s", c.OAuth2.PasswordConnector)
}
if len(c.Web.AllowedOrigins) > 0 {
logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins)
}
Expand All @@ -218,6 +221,7 @@ func serve(cmd *cobra.Command, args []string) error {
serverConfig := server.Config{
SupportedResponseTypes: c.OAuth2.ResponseTypes,
SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
PasswordConnector: c.OAuth2.PasswordConnector,
AllowedOrigins: c.Web.AllowedOrigins,
Issuer: c.Issuer,
Storage: s,
Expand Down
2 changes: 1 addition & 1 deletion cmd/dex/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/spf13/cobra"

"github.com/dexidp/dex/version"
"github.com/concourse/dex/version"
)

func commandVersion() *cobra.Command {
Expand Down
2 changes: 1 addition & 1 deletion connector/authproxy/authproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

"github.com/sirupsen/logrus"

"github.com/dexidp/dex/connector"
"github.com/concourse/dex/connector"
)

// Config holds the configuration parameters for a connector which returns an
Expand Down
303 changes: 303 additions & 0 deletions connector/cf/cf.go
Original file line number Diff line number Diff line change
@@ -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/concourse/dex/connector"
"github.com/sirupsen/logrus"
"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 logrus.FieldLogger
}

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 logrus.FieldLogger) (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.Name, _ = userInfoResult["user_name"].(string)
identity.Username, _ = 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
}
Loading