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

feat: add middleware layer. #1841

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ proto: bin/protoc bin/protoc-gen-go
@cp api/v2/*.proto api/
@./bin/protoc --go_out=plugins=grpc:. --plugin=protoc-gen-go=./bin/protoc-gen-go api/*.proto
@./bin/protoc --go_out=. --plugin=protoc-gen-go=./bin/protoc-gen-go server/internal/*.proto
@./bin/protoc --go_out=plugins=grpc:. --plugin=protoc-gen-go=./bin/protoc-gen-go middleware/grpc/api/*.proto

.PHONY: verify-proto
verify-proto: proto
Expand Down
82 changes: 75 additions & 7 deletions cmd/dex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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 @@ -236,6 +239,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 @@ -247,6 +252,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 @@ -268,10 +275,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 @@ -283,10 +291,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 @@ -240,6 +240,28 @@ func runServe(options serveOptions) 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
2 changes: 2 additions & 0 deletions connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type Identity struct {

Groups []string

CustomClaims map[string]interface{}

// ConnectorData holds data used by the connector for subsequent requests after initial
// authentication, such as access tokens for upstream provides.
//
Expand Down
163 changes: 163 additions & 0 deletions middleware/claims/claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Package claims implements support for manipulating custom claims.
package claims

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

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

// Config holds the configuration parameters for the claims middleware.
// The claims middleware provides the ability to filter and prefix custom claims
// returned by things further up the chain.
//
// An example config:
//
// type: claims
// config:
// actions:
// - discard: "^foo\.example\.com/.*$"
// - rename:
// pattern: "^(.*)\.example\.com/(.*)$"
// as: "example.com/$1/$2"
// - stripPrefix: "foo/"
// - addPrefix: "example.com/"
// inject:
// "example.com/foo": bar
// "example.com/foobar": true
//
// Note that this middleware will *not* affect the standard claims made by
// Dex. It will only manipulate custom claims. We don't really anticipate
// much use of this middleware, because custom claims will more often be
// injected on the basis of external logic, which is more appropriately handled
// via gRPC.
//
type Config struct {
// A list of actions to perform on each claim
Actions []Action `json:"actions,omitempty"`

// Additional claims to inject
Inject map[string]interface{} `json:"inject,omitempty"`
}

// Renames claims matching the regular expression Pattern with As
type RenameAction struct {
Pattern string `json:"pattern"`
As string `json:"as"`
}

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

// Renames claims using a regexp
Rename *RenameAction `json:"rename,omitempty"`

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

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

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

type action struct {
Discard *regexp.Regexp
Rename *renameAction
StripPrefix string
AddPrefix string
}

// Open returns a claims 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("claims: unable to compile discard regexp %q: %v", a.Discard, err)
}
actions[n].Discard = re
}

if a.Rename != nil {
re, err := regexp.Compile(a.Rename.Pattern)
if err != nil {
return nil, fmt.Errorf("claims: unable to compile rename regexp %q: %v", a.Rename.Pattern, err)
}
actions[n].Rename = &renameAction{
Regexp: re,
As: a.Rename.As,
}
}
}

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

type claimsMiddleware struct {
Config

CompiledActions []action
}

// Apply the actions to the claims in the incoming identity
func (c *claimsMiddleware) Process(ctx context.Context, identity connector.Identity) (connector.Identity, error) {
newClaims := map[string]interface{}{}

for claim, value := range identity.CustomClaims {
discard := false

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

if action.Rename != nil {
claim = action.Rename.Regexp.ReplaceAllString(claim,
action.Rename.As)
}

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

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

if !discard {
newClaims[claim] = value
}
}

for claim, value := range c.Inject {
newClaims[claim] = value
}

identity.CustomClaims = newClaims

return identity, nil
}
Loading