mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-03 16:59:22 +02:00
auth0: implement directory provider (#1479)
* add the auth0 directory provider Signed-off-by: Jon Carl <jon.carl@auth0.com> * fix code climate issue: context.Context should be funcs first param Signed-off-by: Jon Carl <jon.carl@auth0.com> * remove unused struct field Signed-off-by: Jon Carl <jon.carl@auth0.com> * remove vendoring Signed-off-by: Jon Carl <jon.carl@auth0.com> * fix auth0 imports and variable name Signed-off-by: Jon Carl <jon.carl@auth0.com>
This commit is contained in:
parent
ec91a98157
commit
f1daf336f6
7 changed files with 750 additions and 1 deletions
203
internal/directory/auth0/auth0.go
Normal file
203
internal/directory/auth0/auth0.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Package auth0 contains the Auth0 directory provider.
|
||||
// Note that Auth0 refers to groups as roles.
|
||||
package auth0
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/auth0.v4/management"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
||||
"github.com/pomerium/pomerium/pkg/grpc/directory"
|
||||
)
|
||||
|
||||
// Name is the provider name.
|
||||
const Name = "auth0"
|
||||
|
||||
// RoleManager defines what is needed to get role info from Auth0.
|
||||
type RoleManager interface {
|
||||
List(opts ...management.ListOption) (r *management.RoleList, err error)
|
||||
Users(id string, opts ...management.ListOption) (u *management.UserList, err error)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
domain string
|
||||
serviceAccount *ServiceAccount
|
||||
newRoleManager func(ctx context.Context, domain string, serviceAccount *ServiceAccount) (RoleManager, error)
|
||||
}
|
||||
|
||||
// Option provides config for the Auth0 Provider.
|
||||
type Option func(cfg *config)
|
||||
|
||||
// WithServiceAccount sets the service account option.
|
||||
func WithServiceAccount(serviceAccount *ServiceAccount) Option {
|
||||
return func(cfg *config) {
|
||||
cfg.serviceAccount = serviceAccount
|
||||
}
|
||||
}
|
||||
|
||||
// WithDomain sets the provider domain option.
|
||||
func WithDomain(domain string) Option {
|
||||
return func(cfg *config) {
|
||||
cfg.domain = domain
|
||||
}
|
||||
}
|
||||
|
||||
func defaultNewRoleManagerFunc(ctx context.Context, domain string, serviceAccount *ServiceAccount) (RoleManager, error) {
|
||||
m, err := management.New(domain, serviceAccount.ClientID, serviceAccount.Secret, management.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth0: could not build management")
|
||||
}
|
||||
return m.Role, nil
|
||||
}
|
||||
|
||||
func getConfig(options ...Option) *config {
|
||||
cfg := &config{
|
||||
newRoleManager: defaultNewRoleManagerFunc,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Provider is an Auth0 user group directory provider.
|
||||
type Provider struct {
|
||||
cfg *config
|
||||
log zerolog.Logger
|
||||
}
|
||||
|
||||
// New creates a new Provider.
|
||||
func New(options ...Option) *Provider {
|
||||
return &Provider{
|
||||
cfg: getConfig(options...),
|
||||
log: log.With().Str("service", "directory").Str("provider", "auth0").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) getRoleManager(ctx context.Context) (RoleManager, error) {
|
||||
return p.cfg.newRoleManager(ctx, p.cfg.domain, p.cfg.serviceAccount)
|
||||
}
|
||||
|
||||
// UserGroups fetches a slice of groups and users.
|
||||
func (p *Provider) UserGroups(ctx context.Context) ([]*directory.Group, []*directory.User, error) {
|
||||
rm, err := p.getRoleManager(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("auth0: could not get the role manager: %w", err)
|
||||
}
|
||||
|
||||
roles, err := getRoles(rm)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("auth0: %w", err)
|
||||
}
|
||||
|
||||
userIDToGroups := map[string][]string{}
|
||||
for _, role := range roles {
|
||||
ids, err := getRoleUserIDs(rm, role.Id)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("auth0: %w", err)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
userIDToGroups[id] = append(userIDToGroups[id], role.Id)
|
||||
}
|
||||
}
|
||||
|
||||
var users []*directory.User
|
||||
for userID, groups := range userIDToGroups {
|
||||
sort.Strings(groups)
|
||||
users = append(users, &directory.User{
|
||||
Id: databroker.GetUserID(Name, userID),
|
||||
GroupIds: groups,
|
||||
})
|
||||
}
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].Id < users[j].Id
|
||||
})
|
||||
return roles, users, nil
|
||||
}
|
||||
|
||||
func getRoles(rm RoleManager) ([]*directory.Group, error) {
|
||||
roles := []*directory.Group{}
|
||||
|
||||
shouldContinue := true
|
||||
page := 0
|
||||
|
||||
for shouldContinue {
|
||||
listRes, err := rm.List(management.IncludeTotals(true), management.Page(page))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not list roles: %w", err)
|
||||
}
|
||||
|
||||
for _, role := range listRes.Roles {
|
||||
roles = append(roles, &directory.Group{
|
||||
Id: *role.ID,
|
||||
Name: *role.Name,
|
||||
})
|
||||
}
|
||||
|
||||
page++
|
||||
shouldContinue = listRes.HasNext()
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func getRoleUserIDs(rm RoleManager, roleID string) ([]string, error) {
|
||||
var ids []string
|
||||
|
||||
shouldContinue := true
|
||||
page := 0
|
||||
|
||||
for shouldContinue {
|
||||
usersRes, err := rm.Users(roleID, management.IncludeTotals(true), management.Page(page))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get users for role %q: %w", roleID, err)
|
||||
}
|
||||
|
||||
for _, user := range usersRes.Users {
|
||||
ids = append(ids, *user.ID)
|
||||
}
|
||||
|
||||
page++
|
||||
shouldContinue = usersRes.HasNext()
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// A ServiceAccount is used by the Auth0 provider to query the API.
|
||||
type ServiceAccount struct {
|
||||
ClientID string `json:"client_id"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// ParseServiceAccount parses the service account in the config options.
|
||||
func ParseServiceAccount(rawServiceAccount string) (*ServiceAccount, error) {
|
||||
bs, err := base64.StdEncoding.DecodeString(rawServiceAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth0: could not decode base64: %w", err)
|
||||
}
|
||||
|
||||
var serviceAccount ServiceAccount
|
||||
if err := json.Unmarshal(bs, &serviceAccount); err != nil {
|
||||
return nil, fmt.Errorf("auth0: could not unmarshal json: %w", err)
|
||||
}
|
||||
|
||||
if serviceAccount.ClientID == "" {
|
||||
return nil, errors.New("auth0: client_id is required")
|
||||
}
|
||||
|
||||
if serviceAccount.Secret == "" {
|
||||
return nil, errors.New("auth0: secret is required")
|
||||
}
|
||||
|
||||
return &serviceAccount, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue