diff --git a/pkg/filter/headerlookup/headerlookup.go b/pkg/filter/headerlookup/headerlookup.go index 91706e1b6a..52b8da2aa7 100644 --- a/pkg/filter/headerlookup/headerlookup.go +++ b/pkg/filter/headerlookup/headerlookup.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "net/http" + "regexp" "strings" "time" @@ -55,6 +56,7 @@ type ( spec *Spec etcdPrefix string headerKey string + pathRegExp *regexp.Regexp cache *lru.Cache cluster cluster.Cluster @@ -71,9 +73,15 @@ type ( // Spec defines header key and etcd prefix that form etcd key like /custom-data/{etcdPrefix}/{headerKey's value}. // This /custom-data/{etcdPrefix}/{headerKey's value} is retrieved from etcd and HeaderSetters extract keys from the // from the retrieved etcd item. + // When PathRegExp is defined, PathRegExp is used with `regexp.FindStringSubmatch` to identify a group from path. + // The first captured group is appended to the etcd key in following format: + // /custom-data/{etcdPrefix}/{headerKey's value}-{regex group} . For example, for path + // "/api/bananas/33" and pathRegExp: "^/api/([a-z]+)/[0-9]*", the group "bananas" is extracted and etcd key is + // /custom-data/{etcdPrefix}/{headerKey's value}-bananas. Spec struct { HeaderKey string `yaml:"headerKey" jsonschema:"required"` EtcdPrefix string `yaml:"etcdPrefix" jsonschema:"required"` + PathRegExp string `yaml:"pathRegExp" jsonschema:"omitempty"` HeaderSetters []*HeaderSetterSpec `yaml:"headerSetters" jsonschema:"required"` } ) @@ -97,6 +105,10 @@ func (spec Spec) Validate() error { return fmt.Errorf("headerSetters[i].headerKey is required") } } + + if _, err := regexp.Compile(spec.PathRegExp); err != nil { + return err + } return nil } @@ -130,6 +142,7 @@ func (hl *HeaderLookup) Init(filterSpec *httppipeline.FilterSpec) { hl.headerKey = http.CanonicalHeaderKey(hl.spec.HeaderKey) hl.cache, _ = lru.New(cacheSize) hl.stopCtx, hl.cancel = context.WithCancel(context.Background()) + hl.pathRegExp = regexp.MustCompile(hl.spec.PathRegExp) hl.watchChanges() } @@ -249,6 +262,12 @@ func (hl *HeaderLookup) handle(ctx httpcontext.HTTPContext) string { logger.Warnf("request does not have header '%s'", hl.spec.HeaderKey) return "" } + if hl.spec.PathRegExp != "" { + path := ctx.Request().Path() + if match := hl.pathRegExp.FindStringSubmatch(path); match != nil && len(match) > 1 { + headerVal = headerVal + "-" + match[1] + } + } headersToAdd, err := hl.lookup(headerVal) if err != nil { logger.Errorf(err.Error()) diff --git a/pkg/filter/headerlookup/headerlookup_test.go b/pkg/filter/headerlookup/headerlookup_test.go index 2c3fce5f4d..0859d79abf 100644 --- a/pkg/filter/headerlookup/headerlookup_test.go +++ b/pkg/filter/headerlookup/headerlookup_test.go @@ -106,6 +106,24 @@ headerKey: "X-AUTH-USER" etcdPrefix: "/credentials/" headerSetters: - etcdKey: "ext-id" +`, + ` +name: headerLookup +kind: HeaderLookup +headerKey: "X-AUTH-USER" +etcdPrefix: "/credentials/" +headerSetters: + - headerKey: "X-ext-id" +`, + ` +name: headerLookup +kind: HeaderLookup +headerKey: "X-AUTH-USER" +pathRegExp: "**" +etcdPrefix: "/credentials/" +headerSetters: + - headerKey: "X-ext-id" + etcdKey: "ext-id" `, } @@ -236,6 +254,73 @@ extra-entry: "extra" } } + if hl.Status() != nil { + t.Errorf("status should be nil") + } + if len(hl.Description()) == 0 { + t.Errorf("description should not be empty") + } + hl.Close() + wg := &sync.WaitGroup{} + wg.Add(1) + clusterInstance.CloseServer(wg) + wg.Wait() +} + +func TestHandleWithPath(t *testing.T) { + etcdDirName, err := ioutil.TempDir("", "etcd-headerlookup-path-test") + check(err) + defer os.RemoveAll(etcdDirName) + const config = ` +name: headerLookup +kind: HeaderLookup +headerKey: "X-AUTH-USER" +etcdPrefix: "credentials/" +pathRegExp: "^/api/([a-z]+)/[0-9]*" +headerSetters: + - etcdKey: "ext-id" + headerKey: "user-ext-id" +` + clusterInstance := cluster.CreateClusterForTest(etcdDirName) + var mockMap sync.Map + supervisor := supervisor.NewMock( + nil, clusterInstance, mockMap, mockMap, nil, nil, false, nil, nil) + + // let's put data to 'bob' + clusterInstance.Put("/custom-data/credentials/bob-bananas", + ` +ext-id: 333 +extra-entry: "extra" +`) + clusterInstance.Put("/custom-data/credentials/bob-pearls", + ` +ext-id: 4444 +extra-entry: "extra" +`) + hl, err := createHeaderLookup(config, nil, supervisor) + check(err) + + ctx, header := prepareCtxAndHeader() + header.Set("X-AUTH-USER", "bob") + hl.Handle(ctx) // path does not match + if header.Get("user-ext-id") != "" { + t.Errorf("failed") + } + ctx.MockedRequest.MockedPath = func() string { + return "/api/bananas/9281" + } + hl.Handle(ctx) + if header.Get("user-ext-id") != "333" { + t.Errorf("failed") + } + ctx.MockedRequest.MockedPath = func() string { + return "/api/pearls/" + } + hl.Handle(ctx) + if header.Get("user-ext-id") != "4444" { + t.Errorf("failed") + } + hl.Close() wg := &sync.WaitGroup{} wg.Add(1)