pomerium/internal/directory/ping/provider.go
Caleb Doxsey fd97561ab1
ping: identity and directory providers (#1975)
* ping: add identity provider

* ping: implement directory provider

* ping, not onelogin

* ping, not onelogin

* escape path params
2021-03-10 16:25:49 -07:00

167 lines
3.8 KiB
Go

// Package ping implements a directory provider for Ping.
package ping
import (
"context"
"fmt"
"net/http"
"net/url"
"sort"
"sync"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"github.com/pomerium/pomerium/pkg/grpc/directory"
)
// Name is the name of the Ping provider.
const Name = "ping"
// Provider implements a directory provider using the Ping API.
type Provider struct {
cfg *config
mu sync.RWMutex
token *oauth2.Token
}
// New creates a new Ping Provider.
func New(options ...Option) *Provider {
cfg := getConfig(options...)
return &Provider{
cfg: cfg,
}
}
// User returns a user's directory information.
func (p *Provider) User(ctx context.Context, userID, accessToken string) (*directory.User, error) {
client, err := p.getClient(ctx)
if err != nil {
return nil, err
}
au, err := getUser(ctx, client, p.cfg.apiURL, p.cfg.environmentID, userID)
if err != nil {
return nil, err
}
return &directory.User{
Id: au.ID,
DisplayName: au.getDisplayName(),
Email: au.Email,
GroupIds: au.MemberOfGroupIDs,
}, nil
}
// UserGroups returns all the users and groups in the directory.
func (p *Provider) UserGroups(ctx context.Context) ([]*directory.Group, []*directory.User, error) {
client, err := p.getClient(ctx)
if err != nil {
return nil, nil, err
}
apiGroups, err := getAllGroups(ctx, client, p.cfg.apiURL, p.cfg.environmentID)
if err != nil {
return nil, nil, err
}
directoryUserLookup := map[string]*directory.User{}
directoryGroups := make([]*directory.Group, len(apiGroups))
for i, ag := range apiGroups {
dg := &directory.Group{
Id: ag.ID,
Name: ag.Name,
}
apiUsers, err := getGroupUsers(ctx, client, p.cfg.apiURL, p.cfg.environmentID, ag.ID)
if err != nil {
return nil, nil, err
}
for _, au := range apiUsers {
du, ok := directoryUserLookup[au.ID]
if !ok {
du = &directory.User{
Id: au.ID,
DisplayName: au.getDisplayName(),
Email: au.Email,
}
directoryUserLookup[au.ID] = du
}
du.GroupIds = append(du.GroupIds, ag.ID)
}
directoryGroups[i] = dg
}
sort.Slice(directoryGroups, func(i, j int) bool {
return directoryGroups[i].Id < directoryGroups[j].Id
})
directoryUsers := make([]*directory.User, 0, len(directoryUserLookup))
for _, du := range directoryUserLookup {
directoryUsers = append(directoryUsers, du)
}
sort.Slice(directoryUsers, func(i, j int) bool {
return directoryUsers[i].Id < directoryUsers[j].Id
})
return directoryGroups, directoryUsers, nil
}
func (p *Provider) getClient(ctx context.Context) (*http.Client, error) {
token, err := p.getToken(ctx)
if err != nil {
return nil, err
}
client := new(http.Client)
*client = *p.cfg.httpClient
client.Transport = &oauth2.Transport{
Source: oauth2.StaticTokenSource(token),
Base: p.cfg.httpClient.Transport,
}
return client, nil
}
func (p *Provider) getToken(ctx context.Context) (*oauth2.Token, error) {
if p.cfg.serviceAccount == nil {
return nil, fmt.Errorf("ping: service account is required")
}
environmentID := p.cfg.serviceAccount.EnvironmentID
if environmentID == "" {
environmentID = p.cfg.environmentID
}
if environmentID == "" {
return nil, fmt.Errorf("ping: environment ID is required")
}
p.mu.RLock()
token := p.token
p.mu.RUnlock()
if token != nil && token.Valid() {
return token, nil
}
p.mu.Lock()
defer p.mu.Unlock()
token = p.token
if token != nil && token.Valid() {
return token, nil
}
ocfg := &clientcredentials.Config{
ClientID: p.cfg.serviceAccount.ClientID,
ClientSecret: p.cfg.serviceAccount.ClientSecret,
TokenURL: p.cfg.authURL.ResolveReference(&url.URL{
Path: fmt.Sprintf("/%s/as/token", environmentID),
}).String(),
}
var err error
p.token, err = ocfg.Token(ctx)
if err != nil {
return nil, err
}
return p.token, nil
}