Get groups from directory api

This commit is contained in:
Joel Speed 2018-02-04 17:17:17 +00:00
parent 36370f8f2a
commit 3f55e2da72
No known key found for this signature in database
GPG Key ID: 6E80578D6751DEFB

View File

@ -5,14 +5,17 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"time" "time"
"github.com/coreos/go-oidc" "github.com/coreos/go-oidc"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/dexidp/dex/connector" "github.com/dexidp/dex/connector"
"github.com/dexidp/dex/pkg/log"
"golang.org/x/oauth2/google"
"google.golang.org/api/admin/directory/v1"
) )
const ( const (
@ -30,11 +33,21 @@ type Config struct {
// Optional list of whitelisted domains // Optional list of whitelisted domains
// If this field is nonempty, only users from a listed domain will be allowed to log in // If this field is nonempty, only users from a listed domain will be allowed to log in
HostedDomains []string `json:"hostedDomains"` HostedDomains []string `json:"hostedDomains"`
// Optional path to service account json
// If nonempty, and groups claim is made, will use authentication from file to
// check groups with the admin directory api
ServiceAccountFilePath string `json:"serviceAccountFilePath"`
// Required if ServiceAccountFilePath
// The email of a GSuite super user which the service account will impersonate
// when listing groups
AdminEmail string
} }
// Open returns a connector which can be used to login users through an upstream // Open returns a connector which can be used to login users through an upstream
// OpenID Connect provider. // OpenID Connect provider.
func (c *Config) Open(id string, logger logrus.FieldLogger) (conn connector.Connector, err error) { func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
provider, err := oidc.NewProvider(ctx, issuerURL) provider, err := oidc.NewProvider(ctx, issuerURL)
@ -66,6 +79,8 @@ func (c *Config) Open(id string, logger logrus.FieldLogger) (conn connector.Conn
logger: logger, logger: logger,
cancel: cancel, cancel: cancel,
hostedDomains: c.HostedDomains, hostedDomains: c.HostedDomains,
serviceAccountFilePath: c.ServiceAccountFilePath,
adminEmail: c.AdminEmail,
}, nil }, nil
} }
@ -80,8 +95,10 @@ type googleConnector struct {
verifier *oidc.IDTokenVerifier verifier *oidc.IDTokenVerifier
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
logger logrus.FieldLogger logger log.Logger
hostedDomains []string hostedDomains []string
serviceAccountFilePath string
adminEmail string
} }
func (c *googleConnector) Close() error { func (c *googleConnector) Close() error {
@ -131,7 +148,7 @@ func (c *googleConnector) HandleCallback(s connector.Scopes, r *http.Request) (i
return identity, fmt.Errorf("google: failed to get token: %v", err) return identity, fmt.Errorf("google: failed to get token: %v", err)
} }
return c.createIdentity(r.Context(), identity, token) return c.createIdentity(r.Context(), identity, s, token)
} }
// Refresh is implemented for backwards compatibility, even though it's a no-op. // Refresh is implemented for backwards compatibility, even though it's a no-op.
@ -145,10 +162,10 @@ func (c *googleConnector) Refresh(ctx context.Context, s connector.Scopes, ident
return identity, fmt.Errorf("google: failed to get token: %v", err) return identity, fmt.Errorf("google: failed to get token: %v", err)
} }
return c.createIdentity(ctx, identity, token) return c.createIdentity(ctx, identity, s, token)
} }
func (c *googleConnector) createIdentity(ctx context.Context, identity connector.Identity, token *oauth2.Token) (connector.Identity, error) { func (c *googleConnector) createIdentity(ctx context.Context, identity connector.Identity, s connector.Scopes, token *oauth2.Token) (connector.Identity, error) {
rawIDToken, ok := token.Extra("id_token").(string) rawIDToken, ok := token.Extra("id_token").(string)
if !ok { if !ok {
return identity, errors.New("google: no id_token in token response") return identity, errors.New("google: no id_token in token response")
@ -182,12 +199,63 @@ func (c *googleConnector) createIdentity(ctx context.Context, identity connector
} }
} }
var groups []string
if s.Groups {
groups, err = c.getGroups(claims.Email)
if err != nil {
return identity, fmt.Errorf("google: could not retrieve groups: %v", err)
}
}
identity = connector.Identity{ identity = connector.Identity{
UserID: idToken.Subject, UserID: idToken.Subject,
Username: claims.Username, Username: claims.Username,
Email: claims.Email, Email: claims.Email,
EmailVerified: claims.EmailVerified, EmailVerified: claims.EmailVerified,
ConnectorData: []byte(token.RefreshToken), ConnectorData: []byte(token.RefreshToken),
Groups: groups,
} }
return identity, nil return identity, nil
} }
func (c *googleConnector) getGroups(email string) ([]string, error) {
srv, err := createDirectoryService(c.serviceAccountFilePath, c.adminEmail)
if err != nil {
return nil, fmt.Errorf("could not create directory service: %v", err)
}
groupsList, err := srv.Groups.List().UserKey(email).Do()
if err != nil {
return nil, fmt.Errorf("could not list groups: %v", err)
}
var userGroups []string
for _, group := range groupsList.Groups {
userGroups = append(userGroups, group.Email)
}
return userGroups, nil
}
func createDirectoryService(serviceAccountFilePath string, email string) (*admin.Service, error) {
jsonCredentials, err := ioutil.ReadFile(serviceAccountFilePath)
if err != nil {
return nil, fmt.Errorf("error reading credentials from file: %v", err)
}
config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupReadonlyScope)
if err != nil {
return nil, fmt.Errorf("unable to parse client secret file to config: %v", err)
}
config.Subject = email
ctx := context.Background()
client := config.Client(ctx)
srv, err := admin.New(client)
if err != nil {
return nil, fmt.Errorf("unable to create directory service %v", err)
}
return srv, nil
}