Skip to content

Commit

Permalink
feat: add middleware layer.
Browse files Browse the repository at this point in the history
Added a Middleware layer that can be used to alter the results of authentication
by a connector.

Also added storage support for the new Middleware layer.

Signed-off-by: Alastair Houghton <[email protected]>
Issues: dexidp#1635
  • Loading branch information
al45tair committed Nov 13, 2020
1 parent 71bbbee commit f6cc2b2
Show file tree
Hide file tree
Showing 17 changed files with 2,364 additions and 37 deletions.
82 changes: 75 additions & 7 deletions cmd/dex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type Config struct {
// Write operations, like updating a connector, will fail.
StaticConnectors []Connector `json:"connectors"`

// StaticMiddleware are global middleware specified in the ConfigMap.
StaticMiddleware []Middleware `json:"middleware"`

// StaticClients cause the server to use this list of clients rather than
// querying the storage. Write operations, like creating a client, will fail.
StaticClients []storage.Client `json:"staticClients"`
Expand Down Expand Up @@ -218,6 +221,8 @@ type Connector struct {
ID string `json:"id"`

Config server.ConnectorConfig `json:"config"`

Middleware []Middleware `json:"middleware"`
}

// UnmarshalJSON allows Connector to implement the unmarshaler interface to
Expand All @@ -229,6 +234,8 @@ func (c *Connector) UnmarshalJSON(b []byte) error {
ID string `json:"id"`

Config json.RawMessage `json:"config"`

Middleware []Middleware `json:"middleware"`
}
if err := json.Unmarshal(b, &conn); err != nil {
return fmt.Errorf("parse connector: %v", err)
Expand All @@ -246,10 +253,11 @@ func (c *Connector) UnmarshalJSON(b []byte) error {
}
}
*c = Connector{
Type: conn.Type,
Name: conn.Name,
ID: conn.ID,
Config: connConfig,
Type: conn.Type,
Name: conn.Name,
ID: conn.ID,
Config: connConfig,
Middleware: conn.Middleware,
}
return nil
}
Expand All @@ -261,10 +269,70 @@ func ToStorageConnector(c Connector) (storage.Connector, error) {
return storage.Connector{}, fmt.Errorf("failed to marshal connector config: %v", err)
}

mwares := make([]storage.Middleware, len(c.Middleware))
for n, mware := range c.Middleware {
mwares[n], err = ToStorageMiddleware(mware)
if err != nil {
return storage.Connector{}, fmt.Errorf("failed to marshal connector middleware: %v", err)
}
}

return storage.Connector{
ID: c.ID,
Type: c.Type,
Name: c.Name,
ID: c.ID,
Type: c.Type,
Name: c.Name,
Config: data,
Middleware: mwares,
}, nil
}

// Middleware is another magic type, like Connector, that can unmarshal YAML
// dynamically. The Type field determines the middleware type, which is then
// customized for Config.
type Middleware struct {
Type string `json:"type"`

Config server.MiddlewareConfig `json:"config"`
}

// UnmarshalJSON allows Connector to implement the unmarshaler interface to
// dynamically determine the type of the middleware config.
func (m *Middleware) UnmarshalJSON(b []byte) error {
var mware struct {
Type string `json:"type"`

Config json.RawMessage `json:"config"`
}
if err := json.Unmarshal(b, &mware); err != nil {
return fmt.Errorf("parse middleware: %v", err)
}
f, ok := server.MiddlewaresConfig[mware.Type]
if !ok {
return fmt.Errorf("unknown middleware type %q", mware.Type)
}

mwareConfig := f()
if len(mware.Config) != 0 {
if err := json.Unmarshal(mware.Config, mwareConfig); err != nil {
return fmt.Errorf("parse middleware config: %v", err)
}
}
*m = Middleware{
Type: mware.Type,
Config: mwareConfig,
}
return nil
}

// ToStorageMiddleware converts an object to storage middleware type.
func ToStorageMiddleware(m Middleware) (storage.Middleware, error) {
data, err := json.Marshal(m.Config)
if err != nil {
return storage.Middleware{}, fmt.Errorf("failed to marshal middleware config: %v", err)
}

return storage.Middleware{
Type: m.Type,
Config: data,
}, nil
}
Expand Down
22 changes: 22 additions & 0 deletions cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,28 @@ func serve(cmd *cobra.Command, args []string) error {

s = storage.WithStaticConnectors(s, storageConnectors)

storageMiddleware := make([]storage.Middleware, len(c.StaticMiddleware))
for i, m := range c.StaticMiddleware {
if m.Type == "" {
return fmt.Errorf("invalid config: Type field is required for a middleware")
}
if m.Config == nil {
return fmt.Errorf("invalid config: no config field for middleware %d (%s)", i, m.Type)
}
logger.Infof("config middleware: %d (%s)", i, m.Type)

// convert to a storage middleware object
mware, err := ToStorageMiddleware(m)
if err != nil {
return fmt.Errorf("failed to initialize storage middleware: %v", err)
}
storageMiddleware[i] = mware
}

if len(storageMiddleware) > 0 {
s = storage.WithStaticMiddleware(s, storageMiddleware)
}

if len(c.OAuth2.ResponseTypes) > 0 {
logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes)
}
Expand Down
186 changes: 186 additions & 0 deletions middleware/groups/groups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Package groups implements support for manipulating groups claims.
package groups

import (
"context"
"fmt"
"regexp"
"sort"
"strings"

"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/middleware"

"github.com/dexidp/dex/pkg/log"
)

// Config holds the configuration parameters for the groups middleware.
// The groups middleware provides the ability to filter and prefix groups
// returned by things further up the chain.
//
// An example config:
//
// type: groups
// config:
// actions:
// - discard: "^admin$"
// - replace:
// pattern: "\s+"
// with: " "
// - stripPrefix: "foo/"
// - addPrefix: "ldap/"
// inject:
// - DexUsers
// - Users
// sorted: true
// unique: true
//
type Config struct {
// A list of actions to perform on each group name
Actions []Action `json:"actions,omitempty"`

// Additional groups to inject
Inject []string `json:"inject,omitempty"`

// If true, sort the resulting list of groups
Sorted bool `json:"sorted,omitempty"`

// If true, ensure that each group is listed at most once
Unique bool `json:"unique,omitempty"`
}

// Replaces matches of the regular expression Pattern with With
type ReplaceAction struct {
Pattern string `json:"pattern"`
With string `json:"with"`
}

// An action
type Action struct {
// Discards groups whose names match a regexp
Discard string `json:"discard,omitempty"`

// Replace regexp matches in a group name
Replace *ReplaceAction `json:"replace,omitempty"`

// Remove a prefix string from a group name
StripPrefix string `json:"stripPrefix,omitempty"`

// Add a prefix string to a group name
AddPrefix string `json:"addPrefix,omitempty"`
}

// The actual Middleware object uses these instead, so that we can pre-compile
// the regular expressions
type replaceAction struct {
Regexp *regexp.Regexp
With string
}

type action struct {
Discard *regexp.Regexp
Replace *replaceAction
StripPrefix string
AddPrefix string
}

// Open returns a groups Middleware
func (c *Config) Open(logger log.Logger) (middleware.Middleware, error) {
// Compile the regular expressions
actions := make([]action, len(c.Actions))
for n, a := range c.Actions {
actions[n] = action{
StripPrefix: a.StripPrefix,
AddPrefix: a.AddPrefix,
}

if a.Discard != "" {
re, err := regexp.Compile(a.Discard)
if err != nil {
return nil, fmt.Errorf("groups: unable to compile discard regexp %q: %v", a.Discard, err)
}
actions[n].Discard = re
}

if a.Replace != nil {
re, err := regexp.Compile(a.Replace.Pattern)
if err != nil {
return nil, fmt.Errorf("groups: unable to compile replace regexp %q: %v", a.Replace.Pattern, err)
}
actions[n].Replace = &replaceAction{
Regexp: re,
With: a.Replace.With,
}
}
}

return &groupsMiddleware{Config: *c, CompiledActions: actions}, nil
}

type groupsMiddleware struct {
Config

CompiledActions []action
}

// Apply the actions to the groups in the incoming identity
func (g *groupsMiddleware) Process(ctx context.Context, identity connector.Identity) (connector.Identity, error) {
groupSet := map[string]struct{}{}
exists := struct{}{}
newGroups := []string{}

if g.Unique && g.Inject != nil {
for _, group := range g.Inject {
groupSet[group] = exists
}
}

for _, group := range identity.Groups {
discard := false

for _, action := range g.CompiledActions {
if action.Discard != nil {
if action.Discard.MatchString(group) {
discard = true
break
}
}

if action.Replace != nil {
group = action.Replace.Regexp.ReplaceAllString(group,
action.Replace.With)
}

if action.StripPrefix != "" {
group = strings.TrimPrefix(group, action.StripPrefix)
}

if action.AddPrefix != "" {
group = action.AddPrefix + group
}
}

if !discard && g.Unique {
_, discard = groupSet[group]
if !discard {
groupSet[group] = exists
}
}

if !discard {
newGroups = append(newGroups, group)
}
}

if g.Inject != nil {
newGroups = append(newGroups, g.Inject...)
}

if g.Sorted {
sort.Strings(newGroups)
}

identity.Groups = newGroups

return identity, nil
}
Loading

0 comments on commit f6cc2b2

Please sign in to comment.