mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-17 00:26:36 +03:00
lib/jwt: support array claim values in match_claims
This commit allows to perform JWT claim matching over 1 dimension arrays. It could
be useful from practical standpoint. Because permissions are usually assigned as a list of values.
For example, the following config allows admin access over list of assigned roles for user:
```yaml
match_claims:
access.roles: "admin"
```
JWT token:
```json
{
"access": {
"roles": [
"read",
"write",
"admin"
]
}
}
```
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10647
This commit is contained in:
@@ -31,6 +31,7 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): add `search` parameter and pagination support in `/api/v1/rules` API. See [#10046](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10046).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#vmui): add default pagination to improve the Alerting Rules page experience when vmalert loads thousands of rules. See [#10046](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/10046).
|
||||
* FEATURE: all VictoriaMetrics components: log a warning when an IPv6 listen address (e.g. `[::]:6969`) is specified but `-enableTCP6` is not set. Previously, the server silently listened on IPv4 only. See [#6858](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6858). Thanks to @andriibeee for the contribution.
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): extend JWT [claim matching](https://docs.victoriametrics.com/victoriametrics/vmauth/#jwt-claim-matching) with array claim values support. See [#10647](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10647). Thanks to @andriibeee for the contribution.
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/victoriametrics/vmagent/): allow specifying `basic_auth` in scrape configs without `username`. Previously this resulted in a config error. Now a warning is logged instead. See [#6956](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6956). Thanks to @andriibeee for the contribution.
|
||||
|
||||
* BUGFIX: `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): retry RPC by dialing a new connection instead of reusing a pooled one when the previous attempt fails with `io.EOF`, `broken pipe` or `reset by peer`. This reduces query failures caused by stale connections to restarted vmstorage nodes. See [#10314](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10314)
|
||||
|
||||
@@ -326,10 +326,9 @@ signed with the configured public key.
|
||||
Claim names support dot-notation for traversal of nested JSON objects
|
||||
(a simplified JSONPath-style approach), for example `vm_access.metrics_account_id` matches `{"vm_access": {"metrics_account_id": 1}}` and
|
||||
`security.permissions.0.read` matches `{"security": {"permissions": [{"read": 1}]}}.
|
||||
Claim names must point to a **leaf value**. The only supported leaf values are string, integer, float and boolean. Any other leaf type
|
||||
is treated as not matched.
|
||||
All configured claims must match exactly.
|
||||
Claim match values use regular expression syntax and must fully match the claim value.
|
||||
Claim names must point to a **leaf value** or an **array**. The supported leaf types are string, integer, float and boolean.
|
||||
If the claim value is an array, each scalar element is compared against the match value - the claim matches if any element matches. Objects and nested arrays inside the array are skipped.
|
||||
All configured claims must match and the values use regular expression syntax.
|
||||
|
||||
For example, the following config routes requests based on the `role` claim in the JWT token:
|
||||
|
||||
@@ -382,6 +381,31 @@ users:
|
||||
url_prefix: "http://victoria-metrics-tenant-2:8428/"
|
||||
```
|
||||
|
||||
The following config matches against array claim values.
|
||||
The first user matches a token with claim `{"roles": ["admin"]}`, while the second matches a token with claim `{"roles": ["read"]}` or `{"roles": ["write"]}`.
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- jwt:
|
||||
public_keys:
|
||||
- |
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
-----END PUBLIC KEY-----
|
||||
match_claims:
|
||||
roles: admin
|
||||
url_prefix: "http://victoria-metrics-admin:8428/"
|
||||
- jwt:
|
||||
public_keys:
|
||||
- |
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
-----END PUBLIC KEY-----
|
||||
match_claims:
|
||||
roles: "^(read|write)$"
|
||||
url_prefix: "http://victoria-metrics-readonly:8428/"
|
||||
```
|
||||
|
||||
The following config matches any valid token (no claim filtering),
|
||||
equivalent to the behavior when `match_claims` is omitted:
|
||||
|
||||
|
||||
@@ -310,11 +310,16 @@ func (t *Token) matchClaim(c *Claim) bool {
|
||||
} else {
|
||||
gotV = t.body.allClaims.Get(keys...)
|
||||
}
|
||||
if gotV == nil || gotV.Type() == fastjson.TypeArray || gotV.Type() == fastjson.TypeObject {
|
||||
// key not found or has complex structure
|
||||
if gotV == nil {
|
||||
// fast path
|
||||
return false
|
||||
}
|
||||
if gotV.Type() == fastjson.TypeString {
|
||||
switch gotV.Type() {
|
||||
case fastjson.TypeObject, fastjson.TypeNull:
|
||||
return false
|
||||
case fastjson.TypeArray:
|
||||
return matchClaimArray(c, gotV.GetArray())
|
||||
case fastjson.TypeString:
|
||||
return c.valueRe.Match(gotV.GetStringBytes())
|
||||
}
|
||||
bb := claimValuePool.Get()
|
||||
@@ -326,6 +331,27 @@ func (t *Token) matchClaim(c *Claim) bool {
|
||||
return match
|
||||
}
|
||||
|
||||
func matchClaimArray(c *Claim, elems []*fastjson.Value) bool {
|
||||
bb := claimValuePool.Get()
|
||||
defer claimValuePool.Put(bb)
|
||||
|
||||
for _, elem := range elems {
|
||||
switch elem.Type() {
|
||||
case fastjson.TypeString:
|
||||
if c.valueRe.Match(elem.GetStringBytes()) {
|
||||
return true
|
||||
}
|
||||
case fastjson.TypeObject, fastjson.TypeArray, fastjson.TypeNull:
|
||||
default:
|
||||
bb.B = elem.MarshalTo(bb.B[:0])
|
||||
if c.valueRe.Match(bb.B) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var claimValuePool bytesutil.ByteBufferPool
|
||||
|
||||
// VMAccess return a reference to the VMAccessClaim
|
||||
|
||||
@@ -948,6 +948,12 @@ func TestTokenMatchClaims(t *testing.T) {
|
||||
}
|
||||
f(&tokenWithStrFields, claims, false)
|
||||
|
||||
// string array element match via an index path
|
||||
f(&tokenWithStrFields, map[string]string{"security.nested_array.0.values": "read"}, true)
|
||||
|
||||
// checking array of hashmaps against a string
|
||||
f(&tokenWithStrFields, map[string]string{"security.permissions": "read"}, false)
|
||||
|
||||
// key not found
|
||||
claims = map[string]string{
|
||||
"name": "Zakhar",
|
||||
@@ -1025,4 +1031,50 @@ func TestTokenMatchClaims(t *testing.T) {
|
||||
}
|
||||
f(&tokenObjectFields, claims, false)
|
||||
|
||||
/*
|
||||
{
|
||||
"name": "Test",
|
||||
"roles": ["read", "write", "admin"],
|
||||
"group_ids": [100, 200, 300],
|
||||
"mixed": ["foo", 42, true, null, {"nested": "obj"}, ["inner"]],
|
||||
"access": {"permissions": ["vm_read", "vm_write"]},
|
||||
"empty_arr": [],
|
||||
"vm_access": {"tenant_id": {"project_id": 1, "account_id": 1}}
|
||||
}
|
||||
*/
|
||||
tokenArrayStr := "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImZmZi1sQjl3In0.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzI1NjI1MzMyLCJleHAiOjE3MjU2MjkyMzIsIm5hbWUiOiJUZXN0Iiwicm9sZXMiOlsicmVhZCIsIndyaXRlIiwiYWRtaW4iXSwiZ3JvdXBfaWRzIjpbMTAwLDIwMCwzMDBdLCJtaXhlZCI6WyJmb28iLDQyLHRydWUsbnVsbCx7Im5lc3RlZCI6Im9iaiJ9LFsiaW5uZXIiXV0sImFjY2VzcyI6eyJwZXJtaXNzaW9ucyI6WyJ2bV9yZWFkIiwidm1fd3JpdGUiXX0sImVtcHR5X2FyciI6W10sInZtX2FjY2VzcyI6eyJ0ZW5hbnRfaWQiOnsicHJvamVjdF9pZCI6MSwiYWNjb3VudF9pZCI6MX19fQ.hhI4capDL1wIfEiTP-biVzszFj55yBR_zRUcEisommXSs-whv1XvCMgUc_KGHj0pzO-YpKVooitfgA6PjNp82xFvCvlEsNI96kN-YKyn4wQShzswXJG_mQdYPhSPoD0UzdDXE6soNzgaMjGxpPA6sOhRGJtAKa0BR-eQgIS-8vcI5P4ymX-Geer1XjHM5I2rsPdKFxrwLec2l1qCTYqWuzS7gb3GH_19lBN13IJXdVmu8zXueVXOq_z9TgQVtQQtaWIW1-URmNk3tOvu78_lDjc1W0e7GtevOdkXl7a6NMtD-fl2Gh-S_shJHApqs93JIkdV6QABoH_Uvt9bTMFsmA"
|
||||
var tokenArrayFields Token
|
||||
if err := tokenArrayFields.Parse(tokenArrayStr, false); err != nil {
|
||||
t.Fatalf("BUG: cannot parse JWT token: %s", err)
|
||||
}
|
||||
|
||||
// string array
|
||||
f(&tokenArrayFields, map[string]string{"roles": "^read$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"roles": "^admin$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"roles": "^nobody$"}, false)
|
||||
f(&tokenArrayFields, map[string]string{"roles": "^(read|write)$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"roles": "wr.*"}, true)
|
||||
|
||||
// numeric array
|
||||
f(&tokenArrayFields, map[string]string{"group_ids": "^200$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"group_ids": "^999$"}, false)
|
||||
|
||||
// nested array via a dot path
|
||||
f(&tokenArrayFields, map[string]string{"access.permissions": "^vm_read$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"access.permissions": "^vm_delete$"}, false)
|
||||
|
||||
// mixed array
|
||||
// hashmaps and nested arrays are skipped
|
||||
f(&tokenArrayFields, map[string]string{"mixed": "^foo$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"mixed": "^42$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"mixed": "^true$"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"mixed": "^obj$"}, false)
|
||||
f(&tokenArrayFields, map[string]string{"mixed": "^inner$"}, false)
|
||||
|
||||
// empty array
|
||||
f(&tokenArrayFields, map[string]string{"empty_arr": ".*"}, false)
|
||||
|
||||
// array claim combined with scalar claim
|
||||
f(&tokenArrayFields, map[string]string{"name": "Test", "roles": "admin"}, true)
|
||||
f(&tokenArrayFields, map[string]string{"name": "Test", "roles": "^nobody$"}, false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user