From b3a58db8861375b2fb945379243715c31856f0b2 Mon Sep 17 00:00:00 2001 From: Christian Simon Date: Thu, 26 Nov 2020 13:57:16 +0000 Subject: [PATCH 1/4] Add tenant resolver (https://github.com/cortexproject/cortex/pull/3486) * Add tenant resolver package This implements the multi tenant resolver as described by the [proposal] for multi tenant query-federation. By default it behaves like before, but it's implementation can be swapped out. [proposal]: https://github.com/cortexproject/cortex/pull/3364 Signed-off-by: Christian Simon * Replace usages of `ExtractOrgID` Use TenantID or UserID depending on which of the methods are meant to be used. Signed-off-by: Christian Simon * Replace usages of `ExtractOrgIDFromHTTPRequest` This is replaced by ExtractTenantIDFromHTTPRequest, which makes sure that exactly one tenant ID is set. Signed-off-by: Christian Simon * Add methods to `tenant` package to use resolver directly Signed-off-by: Christian Simon * Remove UserID method from Resolver interface We need a better definition for what we are trying to achieve with UserID before we can add it to the interface Signed-off-by: Christian Simon * Update comment on the TenantID/TenantIDs Signed-off-by: Christian Simon * Improve performance of NormalizeTenantIDs - reduce allocations by reusing the input slice during de-duplication Signed-off-by: Christian Simon --- tenant/resolver.go | 132 ++++++++++++++++++++++++++++++++++++++++ tenant/resolver_test.go | 107 ++++++++++++++++++++++++++++++++ tenant/tenant.go | 89 +++++++++++++++++++++++++++ tenant/tenant_test.go | 42 +++++++++++++ 4 files changed, 370 insertions(+) create mode 100644 tenant/resolver.go create mode 100644 tenant/resolver_test.go create mode 100644 tenant/tenant.go create mode 100644 tenant/tenant_test.go diff --git a/tenant/resolver.go b/tenant/resolver.go new file mode 100644 index 000000000..e5fbea252 --- /dev/null +++ b/tenant/resolver.go @@ -0,0 +1,132 @@ +package tenant + +import ( + "context" + "net/http" + "strings" + + "github.com/weaveworks/common/user" +) + +var defaultResolver Resolver = NewSingleResolver() + +// WithDefaultResolver updates the resolver used for the package methods. +func WithDefaultResolver(r Resolver) { + defaultResolver = r +} + +// TenantID returns exactly a single tenant ID from the context. It should be +// used when a certain endpoint should only support exactly a single +// tenant ID. It returns an error user.ErrNoOrgID if there is no tenant ID +// supplied or user.ErrTooManyOrgIDs if there are multiple tenant IDs present. +// +// ignore stutter warning +//nolint:golint +func TenantID(ctx context.Context) (string, error) { + return defaultResolver.TenantID(ctx) +} + +// TenantIDs returns all tenant IDs from the context. It should return +// normalized list of ordered and distinct tenant IDs (as produced by +// NormalizeTenantIDs). +// +// ignore stutter warning +//nolint:golint +func TenantIDs(ctx context.Context) ([]string, error) { + return defaultResolver.TenantIDs(ctx) +} + +type Resolver interface { + // TenantID returns exactly a single tenant ID from the context. It should be + // used when a certain endpoint should only support exactly a single + // tenant ID. It returns an error user.ErrNoOrgID if there is no tenant ID + // supplied or user.ErrTooManyOrgIDs if there are multiple tenant IDs present. + TenantID(context.Context) (string, error) + + // TenantIDs returns all tenant IDs from the context. It should return + // normalized list of ordered and distinct tenant IDs (as produced by + // NormalizeTenantIDs). + TenantIDs(context.Context) ([]string, error) +} + +// NewSingleResolver creates a tenant resolver, which restricts all requests to +// be using a single tenant only. This allows a wider set of characters to be +// used within the tenant ID and should not impose a breaking change. +func NewSingleResolver() *SingleResolver { + return &SingleResolver{} +} + +type SingleResolver struct { +} + +func (t *SingleResolver) TenantID(ctx context.Context) (string, error) { + //lint:ignore faillint wrapper around upstream method + return user.ExtractOrgID(ctx) +} + +func (t *SingleResolver) TenantIDs(ctx context.Context) ([]string, error) { + //lint:ignore faillint wrapper around upstream method + orgID, err := user.ExtractOrgID(ctx) + if err != nil { + return nil, err + } + return []string{orgID}, err +} + +type MultiResolver struct { +} + +// NewMultiResolver creates a tenant resolver, which allows request to have +// multiple tenant ids submitted separated by a '|' character. This enforces +// further limits on the character set allowed within tenants as detailed here: +// https://cortexmetrics.io/docs/guides/limitations/#tenant-id-naming) +func NewMultiResolver() *MultiResolver { + return &MultiResolver{} +} + +func (t *MultiResolver) TenantID(ctx context.Context) (string, error) { + orgIDs, err := t.TenantIDs(ctx) + if err != nil { + return "", err + } + + if len(orgIDs) > 1 { + return "", user.ErrTooManyOrgIDs + } + + return orgIDs[0], nil +} + +func (t *MultiResolver) TenantIDs(ctx context.Context) ([]string, error) { + //lint:ignore faillint wrapper around upstream method + orgID, err := user.ExtractOrgID(ctx) + if err != nil { + return nil, err + } + + orgIDs := strings.Split(orgID, tenantIDsLabelSeparator) + for _, orgID := range orgIDs { + if err := ValidTenantID(orgID); err != nil { + return nil, err + } + } + + return NormalizeTenantIDs(orgIDs), nil +} + +// ExtractTenantIDFromHTTPRequest extracts a single TenantID through a given +// resolver directly from a HTTP request. +func ExtractTenantIDFromHTTPRequest(req *http.Request) (string, context.Context, error) { + //lint:ignore faillint wrapper around upstream method + _, ctx, err := user.ExtractOrgIDFromHTTPRequest(req) + if err != nil { + return "", nil, err + } + + tenantID, err := defaultResolver.TenantID(ctx) + if err != nil { + return "", nil, err + } + + return tenantID, ctx, nil +} diff --git a/tenant/resolver_test.go b/tenant/resolver_test.go new file mode 100644 index 000000000..69559263b --- /dev/null +++ b/tenant/resolver_test.go @@ -0,0 +1,107 @@ +package tenant + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/weaveworks/common/user" +) + +func strptr(s string) *string { + return &s +} + +type resolverTestCase struct { + name string + headerValue *string + errTenantID error + errTenantIDs error + tenantID string + tenantIDs []string +} + +func (tc *resolverTestCase) test(r Resolver) func(t *testing.T) { + return func(t *testing.T) { + + ctx := context.Background() + if tc.headerValue != nil { + ctx = user.InjectOrgID(ctx, *tc.headerValue) + } + + tenantID, err := r.TenantID(ctx) + if tc.errTenantID != nil { + assert.Equal(t, tc.errTenantID, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.tenantID, tenantID) + } + + tenantIDs, err := r.TenantIDs(ctx) + if tc.errTenantIDs != nil { + assert.Equal(t, tc.errTenantIDs, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.tenantIDs, tenantIDs) + } + } +} + +var commonResolverTestCases = []resolverTestCase{ + { + name: "no-header", + errTenantID: user.ErrNoOrgID, + errTenantIDs: user.ErrNoOrgID, + }, + { + name: "empty", + headerValue: strptr(""), + tenantIDs: []string{""}, + }, + { + name: "single-tenant", + headerValue: strptr("tenant-a"), + tenantID: "tenant-a", + tenantIDs: []string{"tenant-a"}, + }, +} + +func TestSingleResolver(t *testing.T) { + r := NewSingleResolver() + for _, tc := range append(commonResolverTestCases, []resolverTestCase{ + { + name: "multi-tenant", + headerValue: strptr("tenant-a|tenant-b"), + tenantID: "tenant-a|tenant-b", + tenantIDs: []string{"tenant-a|tenant-b"}, + }, + }...) { + t.Run(tc.name, tc.test(r)) + } +} + +func TestMultiResolver(t *testing.T) { + r := NewMultiResolver() + for _, tc := range append(commonResolverTestCases, []resolverTestCase{ + { + name: "multi-tenant", + headerValue: strptr("tenant-a|tenant-b"), + errTenantID: user.ErrTooManyOrgIDs, + tenantIDs: []string{"tenant-a", "tenant-b"}, + }, + { + name: "multi-tenant-wrong-order", + headerValue: strptr("tenant-b|tenant-a"), + errTenantID: user.ErrTooManyOrgIDs, + tenantIDs: []string{"tenant-a", "tenant-b"}, + }, + { + name: "multi-tenant-duplicate-order", + headerValue: strptr("tenant-b|tenant-b|tenant-a"), + errTenantID: user.ErrTooManyOrgIDs, + tenantIDs: []string{"tenant-a", "tenant-b"}, + }, + }...) { + t.Run(tc.name, tc.test(r)) + } +} diff --git a/tenant/tenant.go b/tenant/tenant.go new file mode 100644 index 000000000..102091c78 --- /dev/null +++ b/tenant/tenant.go @@ -0,0 +1,89 @@ +package tenant + +import ( + "errors" + "fmt" + "sort" +) + +var ( + errTenantIDTooLong = errors.New("tenant ID is too long: max 150 characters") +) + +type errTenantIDUnsupportedCharacter struct { + pos int + tenantID string +} + +func (e *errTenantIDUnsupportedCharacter) Error() string { + return fmt.Sprintf( + "tenant ID '%s' contains unsupported character '%c'", + e.tenantID, + e.tenantID[e.pos], + ) +} + +const tenantIDsLabelSeparator = "|" + +// NormalizeTenantIDs is creating a normalized form by sortiing and de-duplicating the list of tenantIDs +func NormalizeTenantIDs(tenantIDs []string) []string { + sort.Strings(tenantIDs) + + count := len(tenantIDs) + if count <= 1 { + return tenantIDs + } + + posOut := 1 + for posIn := 1; posIn < count; posIn++ { + if tenantIDs[posIn] != tenantIDs[posIn-1] { + tenantIDs[posOut] = tenantIDs[posIn] + posOut++ + } + } + + return tenantIDs[0:posOut] +} + +// ValidTenantID +func ValidTenantID(s string) error { + // check if it contains invalid runes + for pos, r := range s { + if !isSupported(r) { + return &errTenantIDUnsupportedCharacter{ + tenantID: s, + pos: pos, + } + } + } + + if len(s) > 150 { + return errTenantIDTooLong + } + + return nil +} + +// this checks if a rune is supported in tenant IDs (according to +// https://cortexmetrics.io/docs/guides/limitations/#tenant-id-naming) +func isSupported(c rune) bool { + // characters + if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') { + return true + } + + // digits + if '0' <= c && c <= '9' { + return true + } + + // special + return c == '!' || + c == '-' || + c == '_' || + c == '.' || + c == '*' || + c == '\'' || + c == '(' || + c == ')' +} diff --git a/tenant/tenant_test.go b/tenant/tenant_test.go new file mode 100644 index 000000000..b242fd77f --- /dev/null +++ b/tenant/tenant_test.go @@ -0,0 +1,42 @@ +package tenant + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidTenantIDs(t *testing.T) { + for _, tc := range []struct { + name string + err *string + }{ + { + name: "tenant-a", + }, + { + name: "ABCDEFGHIJKLMNOPQRSTUVWXYZ-abcdefghijklmnopqrstuvwxyz_0987654321!.*'()", + }, + { + name: "invalid|", + err: strptr("tenant ID 'invalid|' contains unsupported character '|'"), + }, + { + name: strings.Repeat("a", 150), + }, + { + name: strings.Repeat("a", 151), + err: strptr("tenant ID is too long: max 150 characters"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + err := ValidTenantID(tc.name) + if tc.err == nil { + assert.Nil(t, err) + } else { + assert.EqualError(t, err, *tc.err) + } + }) + } +} From 8113e30830801901bd0e1ad333f429dd3d997397 Mon Sep 17 00:00:00 2001 From: Christian Simon Date: Wed, 23 Dec 2020 15:33:36 +0000 Subject: [PATCH 2/4] Add multi tenant query federation (https://github.com/cortexproject/cortex/pull/3250) * Add tenant query federation This experimental feature allows queries to cover data from more than a single Cortex tenant and can be activated by supplying `-tenant-federation.enabled` to all cortex instances. To query multiple tenants a `|` separated tenant list can be specified in the `X-Scope-OrgID` header. The source tenant of a metric will be exposed in the label `__tenant_id__`. Signed-off-by: Christian Simon * Aggregate the limit of maxQueriers correctly This ensures the limit is aggregated correctly of the setting of each individual tenant. It also implements the logic for the v2 query frontend. Signed-off-by: Christian Simon * Fix tenant labels and make LabelNames more efficient Signed-off-by: Christian Simon * Use tsdb_errors for error handling Signed-off-by: Christian Simon * Fix uninitialized label matcher Regexp matcher need to be initialized, this adds a test for regexp matcher and fixes the underlying issue. Signed-off-by: Christian Simon * Use map for filterValuesByMatchers to reduce allocations Signed-off-by: Christian Simon * Address review suggestions Signed-off-by: Christian Simon * Add validation.SmallestPositiveNonZeroIntPerTenant to avoid code duplication Signed-off-by: Christian Simon * Add limitations and experimental status to docs Signed-off-by: Christian Simon * Remove unnecessary cast of defaultTenantLabel Signed-off-by: Christian Simon * Handle query-range limits for multi tenant queries Signed-off-by: Christian Simon * Skip results cache, if multi tenants query Signed-off-by: Christian Simon * Add failint to ensure query path supports multiple tenants To avoid any future regressions in the multi tenant support within the query path, a faillint command tests if TenantID is used and if it finds one, it suggestest using TenantIDs instead> Signed-off-by: Christian Simon * Align CHANGELOG line with the flag description Signed-off-by: Christian Simon * Add a limitation about missing results cache Signed-off-by: Christian Simon --- tenant/tenant.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tenant/tenant.go b/tenant/tenant.go index 102091c78..fa8089890 100644 --- a/tenant/tenant.go +++ b/tenant/tenant.go @@ -1,9 +1,13 @@ package tenant import ( + "context" "errors" "fmt" "sort" + "strings" + + "github.com/weaveworks/common/user" ) var ( @@ -64,6 +68,10 @@ func ValidTenantID(s string) error { return nil } +func JoinTenantIDs(tenantIDs []string) string { + return strings.Join(tenantIDs, tenantIDsLabelSeparator) +} + // this checks if a rune is supported in tenant IDs (according to // https://cortexmetrics.io/docs/guides/limitations/#tenant-id-naming) func isSupported(c rune) bool { @@ -87,3 +95,11 @@ func isSupported(c rune) bool { c == '(' || c == ')' } + +// TenantIDsFromOrgID extracts different tenants from an orgID string value +// +// ignore stutter warning +//nolint:golint +func TenantIDsFromOrgID(orgID string) ([]string, error) { + return TenantIDs(user.InjectOrgID(context.TODO(), orgID)) +} From 3e7aa7e75276a96d6a96afb131a741f1f5210983 Mon Sep 17 00:00:00 2001 From: Christian Simon Date: Wed, 21 Jul 2021 15:04:45 +0200 Subject: [PATCH 3/4] Restrict path segments in TenantIDs (https://github.com/cortexproject/cortex/pull/4375) This prevents paths generated from TenantIDs to become vulnerable to path traversal attacks. CVE-2021-36157 Signed-off-by: Christian Simon --- tenant/resolver.go | 32 ++++++++++++++++++++++++++++--- tenant/resolver_test.go | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/tenant/resolver.go b/tenant/resolver.go index e5fbea252..72517b082 100644 --- a/tenant/resolver.go +++ b/tenant/resolver.go @@ -2,6 +2,7 @@ package tenant import ( "context" + "errors" "net/http" "strings" @@ -59,14 +60,36 @@ func NewSingleResolver() *SingleResolver { type SingleResolver struct { } +// containsUnsafePathSegments will return true if the string is a directory +// reference like `.` and `..` or if any path separator character like `/` and +// `\` can be found. +func containsUnsafePathSegments(id string) bool { + // handle the relative reference to current and parent path. + if id == "." || id == ".." { + return true + } + + return strings.ContainsAny(id, "\\/") +} + +var errInvalidTenantID = errors.New("invalid tenant ID") + func (t *SingleResolver) TenantID(ctx context.Context) (string, error) { //lint:ignore faillint wrapper around upstream method - return user.ExtractOrgID(ctx) + id, err := user.ExtractOrgID(ctx) + if err != nil { + return "", err + } + + if containsUnsafePathSegments(id) { + return "", errInvalidTenantID + } + + return id, nil } func (t *SingleResolver) TenantIDs(ctx context.Context) ([]string, error) { - //lint:ignore faillint wrapper around upstream method - orgID, err := user.ExtractOrgID(ctx) + orgID, err := t.TenantID(ctx) if err != nil { return nil, err } @@ -109,6 +132,9 @@ func (t *MultiResolver) TenantIDs(ctx context.Context) ([]string, error) { if err := ValidTenantID(orgID); err != nil { return nil, err } + if containsUnsafePathSegments(orgID) { + return nil, errInvalidTenantID + } } return NormalizeTenantIDs(orgIDs), nil diff --git a/tenant/resolver_test.go b/tenant/resolver_test.go index 69559263b..4d2da2416 100644 --- a/tenant/resolver_test.go +++ b/tenant/resolver_test.go @@ -64,6 +64,18 @@ var commonResolverTestCases = []resolverTestCase{ tenantID: "tenant-a", tenantIDs: []string{"tenant-a"}, }, + { + name: "parent-dir", + headerValue: strptr(".."), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, + { + name: "current-dir", + headerValue: strptr("."), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, } func TestSingleResolver(t *testing.T) { @@ -75,6 +87,18 @@ func TestSingleResolver(t *testing.T) { tenantID: "tenant-a|tenant-b", tenantIDs: []string{"tenant-a|tenant-b"}, }, + { + name: "containing-forward-slash", + headerValue: strptr("forward/slash"), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, + { + name: "containing-backward-slash", + headerValue: strptr(`backward\slash`), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, }...) { t.Run(tc.name, tc.test(r)) } @@ -101,6 +125,24 @@ func TestMultiResolver(t *testing.T) { errTenantID: user.ErrTooManyOrgIDs, tenantIDs: []string{"tenant-a", "tenant-b"}, }, + { + name: "multi-tenant-with-relative-path", + headerValue: strptr("tenant-a|tenant-b|.."), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, + { + name: "containing-forward-slash", + headerValue: strptr("forward/slash"), + errTenantID: &errTenantIDUnsupportedCharacter{pos: 7, tenantID: "forward/slash"}, + errTenantIDs: &errTenantIDUnsupportedCharacter{pos: 7, tenantID: "forward/slash"}, + }, + { + name: "containing-backward-slash", + headerValue: strptr(`backward\slash`), + errTenantID: &errTenantIDUnsupportedCharacter{pos: 8, tenantID: "backward\\slash"}, + errTenantIDs: &errTenantIDUnsupportedCharacter{pos: 8, tenantID: "backward\\slash"}, + }, }...) { t.Run(tc.name, tc.test(r)) } From 0ebdb1d3d95507d160e31b7d386ee6d65f8fa4d9 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 21 Jul 2021 15:13:02 +0100 Subject: [PATCH 4/4] Restrict path segments in TenantIDs (https://github.com/cortexproject/cortex/pull/4375) (https://github.com/cortexproject/cortex/pull/4376) This prevents paths generated from TenantIDs to become vulnerable to path traversal attacks. CVE-2021-36157 Signed-off-by: Christian Simon --- tenant/resolver.go | 32 ++++++++++++++++++++++++++++--- tenant/resolver_test.go | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/tenant/resolver.go b/tenant/resolver.go index e5fbea252..72517b082 100644 --- a/tenant/resolver.go +++ b/tenant/resolver.go @@ -2,6 +2,7 @@ package tenant import ( "context" + "errors" "net/http" "strings" @@ -59,14 +60,36 @@ func NewSingleResolver() *SingleResolver { type SingleResolver struct { } +// containsUnsafePathSegments will return true if the string is a directory +// reference like `.` and `..` or if any path separator character like `/` and +// `\` can be found. +func containsUnsafePathSegments(id string) bool { + // handle the relative reference to current and parent path. + if id == "." || id == ".." { + return true + } + + return strings.ContainsAny(id, "\\/") +} + +var errInvalidTenantID = errors.New("invalid tenant ID") + func (t *SingleResolver) TenantID(ctx context.Context) (string, error) { //lint:ignore faillint wrapper around upstream method - return user.ExtractOrgID(ctx) + id, err := user.ExtractOrgID(ctx) + if err != nil { + return "", err + } + + if containsUnsafePathSegments(id) { + return "", errInvalidTenantID + } + + return id, nil } func (t *SingleResolver) TenantIDs(ctx context.Context) ([]string, error) { - //lint:ignore faillint wrapper around upstream method - orgID, err := user.ExtractOrgID(ctx) + orgID, err := t.TenantID(ctx) if err != nil { return nil, err } @@ -109,6 +132,9 @@ func (t *MultiResolver) TenantIDs(ctx context.Context) ([]string, error) { if err := ValidTenantID(orgID); err != nil { return nil, err } + if containsUnsafePathSegments(orgID) { + return nil, errInvalidTenantID + } } return NormalizeTenantIDs(orgIDs), nil diff --git a/tenant/resolver_test.go b/tenant/resolver_test.go index 69559263b..4d2da2416 100644 --- a/tenant/resolver_test.go +++ b/tenant/resolver_test.go @@ -64,6 +64,18 @@ var commonResolverTestCases = []resolverTestCase{ tenantID: "tenant-a", tenantIDs: []string{"tenant-a"}, }, + { + name: "parent-dir", + headerValue: strptr(".."), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, + { + name: "current-dir", + headerValue: strptr("."), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, } func TestSingleResolver(t *testing.T) { @@ -75,6 +87,18 @@ func TestSingleResolver(t *testing.T) { tenantID: "tenant-a|tenant-b", tenantIDs: []string{"tenant-a|tenant-b"}, }, + { + name: "containing-forward-slash", + headerValue: strptr("forward/slash"), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, + { + name: "containing-backward-slash", + headerValue: strptr(`backward\slash`), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, }...) { t.Run(tc.name, tc.test(r)) } @@ -101,6 +125,24 @@ func TestMultiResolver(t *testing.T) { errTenantID: user.ErrTooManyOrgIDs, tenantIDs: []string{"tenant-a", "tenant-b"}, }, + { + name: "multi-tenant-with-relative-path", + headerValue: strptr("tenant-a|tenant-b|.."), + errTenantID: errInvalidTenantID, + errTenantIDs: errInvalidTenantID, + }, + { + name: "containing-forward-slash", + headerValue: strptr("forward/slash"), + errTenantID: &errTenantIDUnsupportedCharacter{pos: 7, tenantID: "forward/slash"}, + errTenantIDs: &errTenantIDUnsupportedCharacter{pos: 7, tenantID: "forward/slash"}, + }, + { + name: "containing-backward-slash", + headerValue: strptr(`backward\slash`), + errTenantID: &errTenantIDUnsupportedCharacter{pos: 8, tenantID: "backward\\slash"}, + errTenantIDs: &errTenantIDUnsupportedCharacter{pos: 8, tenantID: "backward\\slash"}, + }, }...) { t.Run(tc.name, tc.test(r)) }