Compare commits
10 Commits
authlib-ba
...
colin-stua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f140eef4b | ||
|
|
29b04bd2ed | ||
|
|
c0fe27406b | ||
|
|
9409af6f1c | ||
|
|
e1c60e0a83 | ||
|
|
1f88aeb91f | ||
|
|
901360dca4 | ||
|
|
2cf485f6bf | ||
|
|
8ff88036e7 | ||
|
|
5eb0e6f432 |
@@ -69,12 +69,12 @@ require (
|
||||
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
@@ -162,14 +162,14 @@ require (
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
|
||||
@@ -177,38 +177,38 @@ github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrK
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZgtAXTND1Kk9OXg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
@@ -637,8 +637,8 @@ github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfU
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
@@ -655,8 +655,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasn
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
|
||||
@@ -108,22 +108,22 @@ require (
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
@@ -229,7 +229,7 @@ require (
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
|
||||
github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect
|
||||
@@ -242,7 +242,7 @@ require (
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
|
||||
@@ -242,20 +242,20 @@ github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrK
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4=
|
||||
@@ -264,12 +264,12 @@ github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFie
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0/go.mod h1:UseIHRfrm7PqeZo6fcTb6FUCXzCnh1KJbQbmOfxArGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJdQgRP5mKCTDt4GNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo=
|
||||
@@ -282,12 +282,12 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZ
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 h1:60m4tnanN1ctzIu4V3bfCNJ39BiOPSm1gHFlFjTkRE0=
|
||||
@@ -853,8 +853,8 @@ github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfU
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 h1:JOzchPgptwJdruYoed7x28lFDwhzs7kssResYsnC0iI=
|
||||
@@ -891,8 +891,8 @@ github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae h1:35W3Wjp
|
||||
github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae/go.mod h1:6CJ1uXmLZ13ufpO9xE4pST+DyaBt0uszzrV0YnoaVLQ=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec h1:wnzJov9RhSHGaTYGzTygL4qq986fLen8xSqnQgaMd28=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec/go.mod h1:j1IY7J2rUz7TcTjFVVx6HCpyTlYOJPtXuGRZ7sI+vSo=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
|
||||
@@ -31,12 +31,12 @@ require (
|
||||
github.com/apache/arrow-go/v18 v18.4.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
@@ -97,14 +97,14 @@ require (
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // indirect
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 // indirect
|
||||
github.com/grafana/otel-profiling-go v0.5.1 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
|
||||
@@ -30,18 +30,18 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
@@ -229,8 +229,8 @@ github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfU
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad0a5JpEL4mH9ry7Ws=
|
||||
@@ -243,8 +243,8 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
|
||||
|
||||
@@ -428,12 +428,25 @@ Or using a Kubernetes format, for example `kubernetes-dashboard.json`:
|
||||
|
||||
You _must_ use the Kubernetes resource format to provision dashboards v2 / dynamic dashboards.
|
||||
|
||||
It later polls that path every `updateIntervalSeconds` for updates to the dashboard files and updates its database.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Grafana installs dashboards at the root level if you don't set the `folder` field.
|
||||
{{< /admonition >}}
|
||||
|
||||
#### Detect updates to provisioned dashboards files
|
||||
|
||||
After Grafana provisions your dashboards, it checks the filesystem for changes and updates dashboards as needed.
|
||||
|
||||
The mechanism Grafana uses to do this depends on your `updateIntervalSeconds` value:
|
||||
|
||||
- **More than 10 seconds**: Grafana polls the path at that interval.
|
||||
- **10 seconds or less**: Grafana watches the filesystem for changes and updates dashboards when it detects them.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
When `updateIntervalSeconds` is 10 or less, Grafana relies on filesystem watch events to detect changes.
|
||||
Depending on your filesystem and how you mount or sync dashboard files (for example, Docker bind mounts or some network filesystems), those events might not reach Grafana.
|
||||
To work around this, set `updateIntervalSeconds` to more than 10 to force polling, or update your setup so filesystem watch events are propagated.
|
||||
{{< /admonition >}}
|
||||
|
||||
#### Make changes to a provisioned dashboard
|
||||
|
||||
You can make changes to a provisioned dashboard in the Grafana UI but its not possible to automatically save the changes back to the provisioning source.
|
||||
|
||||
44
go.mod
44
go.mod
@@ -89,18 +89,18 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20260106131612-bb61e476969f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20260106131612-bb61e476969f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 // @grafana/identity-access-team
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics
|
||||
github.com/grafana/dskit v0.0.0-20251204003651-27988664e6ff // @grafana/grafana-backend-group
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // @grafana/grafana-backend-group
|
||||
github.com/grafana/e2e v0.1.1 // @grafana-app-platform-squad
|
||||
github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f // @grafana/sharing-squad
|
||||
github.com/grafana/gomemcache v0.0.0-20251127154401-74f93547077b // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 // @grafana/aws-datasources
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 // @grafana/aws-datasources
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // @grafana/partner-datasources
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-google-sdk-go v0.4.2 // @grafana/partner-datasources
|
||||
@@ -151,7 +151,7 @@ require (
|
||||
github.com/openfga/api/proto v0.0.0-20250909172242-b4b2a12f5c67 // @grafana/identity-access-team
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20251027165255-0f8f255e5f6c // @grafana/identity-access-team
|
||||
github.com/openfga/openfga v1.11.1 // @grafana/identity-access-team
|
||||
github.com/opentracing-contrib/go-grpc v0.1.2 // @grafana/grafana-search-and-storage
|
||||
github.com/opentracing-contrib/go-grpc v0.1.1 // @grafana/grafana-search-and-storage
|
||||
github.com/opentracing/opentracing-go v1.2.0 // @grafana/grafana-search-and-storage
|
||||
github.com/openzipkin/zipkin-go v0.4.3 // @grafana/oss-big-tent
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // @grafana/alerting-backend
|
||||
@@ -342,23 +342,23 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/at-wat/mqtt-go v0.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 // indirect
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20240507144631-af9851f82b27 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect
|
||||
@@ -456,7 +456,6 @@ require (
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grafana/jsonparser v0.0.0-20240425183733-ea80629e1a32 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grafana/sqlds/v4 v4.2.7 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
github.com/hashicorp/consul/api v1.31.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
@@ -472,7 +471,7 @@ require (
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.3 // indirect
|
||||
github.com/hashicorp/memberlist v0.5.2 // indirect
|
||||
github.com/hashicorp/serf v0.10.2 // indirect
|
||||
github.com/hashicorp/vault/api v1.20.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
@@ -517,7 +516,7 @@ require (
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/mdlayher/vsock v1.2.1 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 // indirect
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
@@ -550,17 +549,17 @@ require (
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/jaeger v0.124.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opentracing-contrib/go-stdlib v1.1.0 // indirect
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.15.0 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.14.0 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
|
||||
@@ -574,7 +573,7 @@ require (
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/sercand/kuberesolver/v6 v6.0.1 // indirect
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/shadowspore/fossil-delta v0.0.0-20241213113458-1d797d70cbe3 // indirect
|
||||
@@ -683,6 +682,7 @@ require (
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect
|
||||
github.com/grafana/sqlds/v5 v5.0.3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
|
||||
58
go.sum
58
go.sum
@@ -854,20 +854,20 @@ github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrK
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.45.3 h1:Nn3qce+OHZuMj/edx4its32uxedAmquCDxtZkrdeiD4=
|
||||
@@ -876,12 +876,12 @@ github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0 h1:e5cbPZYTIY2nUEFie
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.51.0/go.mod h1:UseIHRfrm7PqeZo6fcTb6FUCXzCnh1KJbQbmOfxArGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2 h1:IfMb3Ar8xEaWjgH/zeVHYD8izwJdQgRP5mKCTDt4GNk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ec2 v1.225.2/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.41.2 h1:zJeUxFP7+XP52u23vrp4zMcVhShTWbNO8dHV6xCSvFo=
|
||||
@@ -894,12 +894,12 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0 h1:0reDqfEN+tB+sozj2r92Bep8MEwBZ
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1 h1:w6a0H79HrHf3lr+zrw+pSzR5B+caiQFAKiNHlrUcnoc=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.40.1/go.mod h1:c6Vg0BRiU7v0MVhHupw90RyL120QBwAMLbDCzptGeMk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f/go.mod h1:2stgcRjl6QmW+gU2h5E7BQXg4HU0gzxKWDuT5HviN9s=
|
||||
@@ -1629,20 +1629,14 @@ github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f h1:Br4SaUL3dnVopK
|
||||
github.com/grafana/alerting v0.0.0-20251231150637-b7821017d69f/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f h1:Cbm6OKkOcJ+7CSZsGsEJzktC/SIa5bxVeYKQLuYK86o=
|
||||
github.com/grafana/authlib v0.0.0-20250930082137-a40e2c2b094f/go.mod h1:axY0cdOg3q0TZHwpHnIz5x16xZ8ZBxJHShsSHHXcHQg=
|
||||
github.com/grafana/authlib v0.0.0-20260106131612-bb61e476969f h1:OfVtnO3+Ficm7W69dFD5IaZWlMvOLIWBBnppE99dVkU=
|
||||
github.com/grafana/authlib v0.0.0-20260106131612-bb61e476969f/go.mod h1:KUNx2Qz7mgh2tm2/TJXx0+uq5SkCrquCFI+dHln2Q50=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4 h1:Muoy+FMGrHj3GdFbvsMzUT7eusgii9PKf9L1ZaXDDbY=
|
||||
github.com/grafana/authlib/types v0.0.0-20251119142549-be091cf2f4d4/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20251203163023-dd5a97c606e3/go.mod h1:CZ5McGzO/q6lnRb8xvTODCC2bJniQoQ+gho0AVZC/zY=
|
||||
github.com/grafana/authlib/types v0.0.0-20260106131612-bb61e476969f h1:5ZI6e22sGdg36MAIMJkH6PUHtZU/QuwAScNfgWNlK0I=
|
||||
github.com/grafana/authlib/types v0.0.0-20260106131612-bb61e476969f/go.mod h1:j+YTXmAcD4zCNyl4QSNqYSEe/q9KgrH1btodnhK29hI=
|
||||
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
|
||||
github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg=
|
||||
github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=
|
||||
github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 h1:jSojuc7njleS3UOz223WDlXOinmuLAIPI0z2vtq8EgI=
|
||||
github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4/go.mod h1:VahT+GtfQIM+o8ht2StR6J9g+Ef+C2Vokh5uuSmOD/4=
|
||||
github.com/grafana/dskit v0.0.0-20251204003651-27988664e6ff/go.mod h1:/pHIcyeZJBZbtboXOjRtPaMl5KK+2VRdNJbCHDkpDYs=
|
||||
github.com/grafana/e2e v0.1.1 h1:/b6xcv5BtoBnx8cZnCiey9DbjEc8z7gXHO5edoeRYxc=
|
||||
github.com/grafana/e2e v0.1.1/go.mod h1:RpNLgae5VT+BUHvPE+/zSypmOXKwEu4t+tnEMS1ATaE=
|
||||
github.com/grafana/go-mysql-server v0.20.1-grafana1 h1:yA4Mzt+tTdIlQutBUaiPnepULPQ7CS4hMu2GOpHqT6s=
|
||||
@@ -1651,15 +1645,14 @@ github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f h1:5xkjl5Y/j2QefJKO
|
||||
github.com/grafana/gofpdf v0.0.0-20250307124105-3b9c5d35577f/go.mod h1:+O5QxOwwgP10jedZHapzXY+IPKTnzHBtIs5UUb9G+kI=
|
||||
github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d h1:oXRJlb9UjVsl6LhqBdbyAQ9YFhExwsj4bjh5vwMNRZY=
|
||||
github.com/grafana/gomemcache v0.0.0-20250318131618-74242eea118d/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/gomemcache v0.0.0-20251127154401-74f93547077b/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/grafana-api-golang-client v0.27.0 h1:zIwMXcbCB4n588i3O2N6HfNcQogCNTd/vPkEXTr7zX8=
|
||||
github.com/grafana/grafana-api-golang-client v0.27.0/go.mod h1:uNLZEmgKtTjHBtCQMwNn3qsx2mpMb8zU+7T4Xv3NR9Y=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7 h1:9mF7nqkqP0QUYYDlznoOt+GIyjzj45wGfUHB32u2ZMo=
|
||||
github.com/grafana/grafana-app-sdk v0.48.7/go.mod h1:DWsaaH39ZMHwSOSoUBaeW8paMrRaYsjRYlLwCJYd78k=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7 h1:Oa5qg473gka5+W/WQk61Xbw4YdAv+wV2Z4bJtzeCaQw=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.48.7/go.mod h1:5u3KalezoBAAo2Y3ytDYDAIIPvEqFLLDSxeiK99QxDU=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0 h1:/bfJzP93rCel1GbWoRSq0oUo424MZXt8jAp2BK9w8tM=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2 h1:GrUEoLbs46r8rG/GZL4L2b63Bo+rkIYKdtCT7kT5KkM=
|
||||
github.com/grafana/grafana-aws-sdk v1.4.2/go.mod h1:1qnZdYs6gQzxxF0dDodaE7Rn9fiMzuhwvtaAZ7ySnhY=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 h1:FFcEA01tW+SmuJIuDbHOdgUBL+d7DPrZ2N4zwzPhfGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1/go.mod h1:Oi4anANlCuTCc66jCyqIzfVbgLXFll8Wja+Y4vfANlc=
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 h1:JOzchPgptwJdruYoed7x28lFDwhzs7kssResYsnC0iI=
|
||||
@@ -1698,8 +1691,8 @@ github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrR
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56 h1:SDGrP81Vcd102L3UJEryRd1eestRw73wt+b8vnVEFe0=
|
||||
github.com/grafana/saml v0.4.15-0.20240917091248-ae3bbdad8a56/go.mod h1:S4+611dxnKt8z/ulbvaJzcgSHsuhjVc1QHNTcr1R7Fw=
|
||||
github.com/grafana/sqlds/v4 v4.2.7 h1:sFQhsS7DBakNMdxa++yOfJ9BVvkZwFJ0B95o57K0/XA=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3 h1:+yUMUxfa0WANQsmS9xtTFSRX1Q55Iv1B9EjlrW4VlBU=
|
||||
github.com/grafana/sqlds/v5 v5.0.3/go.mod h1:GKeTTiC+GeR1X0z3f0Iee+hZnNgN62uQpj5XVMx5Uew=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec h1:wnzJov9RhSHGaTYGzTygL4qq986fLen8xSqnQgaMd28=
|
||||
github.com/grafana/tempo v1.5.1-0.20250529124718-87c2dc380cec/go.mod h1:j1IY7J2rUz7TcTjFVVx6HCpyTlYOJPtXuGRZ7sI+vSo=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
@@ -1804,7 +1797,6 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn
|
||||
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
|
||||
github.com/hashicorp/memberlist v0.5.2 h1:rJoNPWZ0juJBgqn48gjy59K5H4rNgvUoM1kUD7bXiuI=
|
||||
github.com/hashicorp/memberlist v0.5.2/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4=
|
||||
github.com/hashicorp/memberlist v0.5.3/go.mod h1:h60o12SZn/ua/j0B6iKAZezA4eDaGsIuPO70eOaJ6WE=
|
||||
github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec h1:+YBzb977VrmffaCX/OBm17dEVJUcWn5dW+eqs3aIJ/A=
|
||||
github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
@@ -2059,7 +2051,6 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE=
|
||||
github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
|
||||
@@ -2214,11 +2205,9 @@ github.com/openfga/openfga v1.11.1 h1:+cJBPi/J+RWPRg+cXOjwWEwjauiW8rdE3kEzcFy1ME
|
||||
github.com/openfga/openfga v1.11.1/go.mod h1:MuTGr/ghY7t2sEGwS/59pq9SkqO0QY1kQLIe8Upt+G8=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.1 h1:Ws7IN1zyiL1DFqKQPhRXuKe5pLYzMfdxnC1qtajE2PE=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.1/go.mod h1:Nu6sz+4zzgxXu8rvKfnwjBEmHsuhTigxRwV2RhELrS8=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.2/go.mod h1:glU6rl1Fhfp9aXUHkE36K2mR4ht8vih0ekOVlWKEUHM=
|
||||
github.com/opentracing-contrib/go-stdlib v0.0.0-20190519235532-cf7a6c988dc9/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w=
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0 h1:TBS7YuVotp8myLon4Pv7BtCBzOTo1DeZCld0Z63mW2w=
|
||||
github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU=
|
||||
github.com/opentracing-contrib/go-stdlib v1.1.0/go.mod h1:S0p+X9p6dcBkoMTL+Qq2VOvxKs9ys5PpYWXWqlCS0bQ=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
@@ -2252,7 +2241,6 @@ github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
@@ -2331,7 +2319,6 @@ github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57J
|
||||
github.com/prometheus/exporter-toolkit v0.11.0/go.mod h1:BVnENhnNecpwoTLiABx7mrPB/OLRIgN74qlQbV+FK1Q=
|
||||
github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=
|
||||
github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA=
|
||||
github.com/prometheus/exporter-toolkit v0.15.0/go.mod h1:OyRWd2iTo6Xge9Kedvv0IhCrJSBu36JCfJ2yVniRIYk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
@@ -2416,7 +2403,6 @@ github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQcc
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0 h1:ScvS2Ga9snVkpOahln/BCLySr3/iBAHJf25u66DweZ0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.0/go.mod h1:Dxkqms3OJadP5zirIBPLi9FV8Qpys3T3w40XPEcVsu0=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.1/go.mod h1:C0tsTuRMONSY+Xf7pv7RMW1/JlewY1+wS8SZE+1lf1s=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
|
||||
33
go.work.sum
33
go.work.sum
@@ -317,7 +317,6 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator v0.53.0 h1:RAHqDHJmNMLe6JvDoRIlXmb72w+62Ue/k5p/qP9yfAg=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/propagator v0.53.0/go.mod h1:dtCRwgvytbGKWdlrjMOg9geBoRwRpCYWIOM/JhVsDIc=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.2.0/go.mod h1:CiIeGiHSd06zjX+FypuEJ5EQ07KKtxZ+8J6hszwVQig=
|
||||
github.com/IBM/go-sdk-core/v5 v5.17.4 h1:VGb9+mRrnS2HpHZFM5hy4J6ppIWnwNrw0G+tLSgcJLc=
|
||||
github.com/IBM/go-sdk-core/v5 v5.17.4/go.mod h1:KsAAI7eStAWwQa4F96MLy+whYSh39JzNjklZRbN/8ns=
|
||||
github.com/IBM/ibm-cos-sdk-go v1.11.0 h1:Jp55NLN3OvBwucMGpP5wNybyjncsmTZ9+GPHai/1cE8=
|
||||
@@ -425,23 +424,30 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1/go.mod h1:MVYeeOhILFFemC/XlYTCl
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 h1:oUEqVqonG3xuarrsze1KVJ30KagNYDemikTbdu8KlN8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5/go.mod h1:VNM08cHlOsIbSHRqb6D/M2L4kKXfJv3A2/f0GNbOQSc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.87 h1:oDPArGgCrG/4aTi86ij3S2PB59XXkTSKYVNQlmqRHXQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.87/go.mod h1:ZeQC4gVarhdcWeM1c90DyBLaBCNhEeAbKUXwVI/byvw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.69/go.mod h1:GJj8mmO6YT6EqgduWocwhMoxTLFitkhIrK+owzrYL2I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw=
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs=
|
||||
@@ -449,11 +455,13 @@ github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 h1:0wOCTKrmwkyC8Bk7
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0 h1:JPXkrQk5OS/+Q81fKH97Ll/Vmmy0p9vwHhxw+V+tVjg=
|
||||
github.com/aws/aws-sdk-go-v2/service/kinesis v1.33.0/go.mod h1:dJngkoVMrq0K7QvRkdRZYM4NUp6cdWa2GBdpm8zoY8U=
|
||||
@@ -487,10 +495,13 @@ github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1 h1:OwMzNDe5VVTXD4kGmeK/FtqAITiV
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.60.1/go.mod h1:IyVabkWrs8SNdOEZLyFFcW9bUltV4G6OQS0s6H20PHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
@@ -880,6 +891,7 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gophercloud/gophercloud v1.13.0 h1:8iY9d1DAbzMW6Vok1AxbbK5ZaUjzMp0tdyt4fX9IeJ0=
|
||||
github.com/gophercloud/gophercloud v1.13.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
@@ -890,23 +902,19 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20250729175202-b4b881b7b263/go.mod h1:VKxaR93Gff0ZlO2sPcdPVob1a/UzArFEW5zx3Bpyhls=
|
||||
github.com/grafana/alerting v0.0.0-20251009192429-9427c24835ae/go.mod h1:VGjS5gDwWEADPP6pF/drqLxEImgeuHlEW5u8E5EfIrM=
|
||||
github.com/grafana/alerting v0.0.0-20251223160021-926c74910196/go.mod h1:l7v67cgP7x72ajB9UPZlumdrHqNztpKoqQ52cU8T3LU=
|
||||
github.com/grafana/authlib v0.0.0-20250710201142-9542f2f28d43/go.mod h1:1fWkOiL+m32NBgRHZtlZGz2ji868tPZACYbqP3nBRJI=
|
||||
github.com/grafana/authlib/types v0.0.0-20250710201142-9542f2f28d43/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20250926065801-df98203cff37/go.mod h1:qeWYbnWzaYGl88JlL9+DsP1GT2Cudm58rLtx13fKZdw=
|
||||
github.com/grafana/authlib/types v0.0.0-20251203163023-dd5a97c606e3 h1:T4AMrL8ZB1U25m/+FOmkqWPnz0X7u/Oqj1ISg4OrS2c=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
|
||||
github.com/grafana/cog v0.0.43/go.mod h1:TDunc7TYF7EfzjwFOlC5AkMe3To/U2KqyyG3QVvrF38=
|
||||
github.com/grafana/dskit v0.0.0-20250611075409-46f51e1ce914/go.mod h1:OiN4P4aC6LwLzLbEupH3Ue83VfQoNMfG48rsna8jI/E=
|
||||
github.com/grafana/dskit v0.0.0-20250818234656-8ff9c6532e85/go.mod h1:kImsvJ1xnmeT9Z6StK+RdEKLzlpzBsKwJbEQfmBJdFs=
|
||||
github.com/grafana/dskit v0.0.0-20251204003651-27988664e6ff h1:eDbrQsfY1Y3vMfuy5suGI2DRNC1DFBcZMFMlNbPrdiE=
|
||||
github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak=
|
||||
github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90=
|
||||
github.com/grafana/go-mysql-server v0.20.1-0.20251027172658-317a8d46ffa4/go.mod h1:EeYR0apo+8j2Dyxmn2ghkPlirO2S5mT1xHBrA+Efys8=
|
||||
github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/gomemcache v0.0.0-20251127154401-74f93547077b h1:5qp8/5YPt/Z2RW5QHsxvwE05+LWQYIXydP2MwOkMfb8=
|
||||
github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM=
|
||||
github.com/grafana/grafana-app-sdk v0.40.2/go.mod h1:BbNXPNki3mtbkWxYqJsyA1Cj9AShSyaY33z8WkyfVv0=
|
||||
github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0UrQVTqtYBU=
|
||||
@@ -938,6 +946,7 @@ github.com/grafana/grafana-aws-sdk v1.0.2 h1:98eBuHYFmgvH0xO9kKf4RBsEsgQRp8EOA/9
|
||||
github.com/grafana/grafana-aws-sdk v1.0.2/go.mod h1:hO7q7yWV+t6dmiyJjMa3IbuYnYkBua+G/IAlOPVIYKE=
|
||||
github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8=
|
||||
github.com/grafana/grafana-aws-sdk v1.2.0/go.mod h1:bBo7qOmM3f61vO+2JxTolNUph1l2TmtzmWcU9/Im+8A=
|
||||
github.com/grafana/grafana-aws-sdk v1.3.0/go.mod h1:VGycF0JkCGKND2O5je1ucOqPJ0ZNhZYzV3c2bNBAaGk=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6/go.mod h1:V7y2BmsWxS3A9Ohebwn4OiSfJJqi//4JQydQ8fHTduo=
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0/go.mod h1:H9sVh9A4yg5egMGZeh0mifxT1Q/uqwKe1LBjBJU6pN8=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.263.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw=
|
||||
@@ -985,6 +994,7 @@ github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20250604130045-92c8f6389b36/go.mod h1:O/QP1BCm0HHIzbKvgMzqb5sSyH88rzkFk84F4TfJjBU=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
|
||||
github.com/grafana/sqlds/v4 v4.2.4/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/sqlds/v4 v4.2.7/go.mod h1:BQRjUG8rOqrBI4NAaeoWrIMuoNgfi8bdhCJ+5cgEfLU=
|
||||
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU=
|
||||
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
@@ -1028,7 +1038,6 @@ github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI
|
||||
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
||||
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||
github.com/hashicorp/memberlist v0.5.3 h1:tQ1jOCypD0WvMemw/ZhhtH+PWpzcftQvgCorLu0hndk=
|
||||
github.com/hashicorp/raft v1.7.0 h1:4u24Qn6lQ6uwziM++UgsyiT64Q8GyRn43CV41qPiz1o=
|
||||
github.com/hashicorp/raft v1.7.0/go.mod h1:N1sKh6Vn47mrWvEArQgILTyng8GoDRNYlgKyK7PMjs0=
|
||||
github.com/hashicorp/raft-wal v0.4.1 h1:aU8XZ6x8R9BAIB/83Z1dTDtXvDVmv9YVYeXxd/1QBSA=
|
||||
@@ -1189,7 +1198,6 @@ github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxW
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/minio/minio-go/v7 v7.0.75/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8=
|
||||
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
|
||||
@@ -1349,8 +1357,6 @@ github.com/openfga/api/proto v0.0.0-20250127102726-f9709139a369/go.mod h1:m74TNg
|
||||
github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe/go.mod h1:5Z0pbTT7Jz/oQFLfadb+C5t5NwHrduAO7j7L07Ec1GM=
|
||||
github.com/openfga/openfga v1.10.0/go.mod h1:6/m4GTwQsqECsGYQVD3t5sCX97rh3smnmxbMa3YAtJk=
|
||||
github.com/opentracing-contrib/go-grpc v0.0.0-20210225150812-73cb765af46e/go.mod h1:DYR5Eij8rJl8h7gblRrOZ8g0kW1umSpKqYIBTgeDtLo=
|
||||
github.com/opentracing-contrib/go-grpc v0.1.2 h1:MP16Ozc59kqqwn1v18aQxpeGZhsBanJ2iurZYaQSZ+g=
|
||||
github.com/opentracing-contrib/go-stdlib v1.1.0 h1:cZBWc4pA4e65tqTJddbflK435S0tDImj6c9BMvkdUH0=
|
||||
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
|
||||
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
@@ -1373,7 +1379,6 @@ github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2
|
||||
github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
|
||||
@@ -1398,7 +1403,6 @@ github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8N
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
|
||||
github.com/prometheus/exporter-toolkit v0.10.1-0.20230714054209-2f4150c63f97/go.mod h1:LoBCZeRh+5hX+fSULNyFnagYlQG/gBsyA/deNzROkq8=
|
||||
github.com/prometheus/exporter-toolkit v0.15.0 h1:Pcle5sSViwR1x0gdPd0wtYrPQENBieQAM7TmT0qtb2U=
|
||||
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ=
|
||||
github.com/prometheus/statsd_exporter v0.26.1 h1:ucbIAdPmwAUcA+dU+Opok8Qt81Aw8HanlO+2N/Wjv7w=
|
||||
@@ -1460,7 +1464,6 @@ github.com/sercand/kuberesolver v2.4.0+incompatible h1:WE2OlRf6wjLxHwNkkFLQGaZcV
|
||||
github.com/sercand/kuberesolver v2.4.0+incompatible/go.mod h1:lWF3GL0xptCB/vCiJPl/ZshwPsX/n4Y7u0CW9E7aQIQ=
|
||||
github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY=
|
||||
github.com/sercand/kuberesolver/v5 v5.1.1/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ=
|
||||
github.com/sercand/kuberesolver/v6 v6.0.1 h1:XZUTA0gy/lgDYp/UhEwv7Js24F1j8NJ833QrWv0Xux4=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
@@ -1851,6 +1854,7 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.62.0/go.mod h1:WfEApdZDMlLUAev/0QQpr8EJ/z0VWDKYZ5tF5RH5T1U=
|
||||
@@ -1959,7 +1963,6 @@ golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sU
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
@@ -1972,7 +1975,7 @@ golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5N
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
|
||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
@@ -2066,6 +2069,7 @@ golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
@@ -2144,7 +2148,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y=
|
||||
|
||||
@@ -699,10 +699,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
playlistsReconciler?: boolean;
|
||||
/**
|
||||
* Enable passwordless login via magic link authentication
|
||||
*/
|
||||
passwordlessMagicLinkAuthentication?: boolean;
|
||||
/**
|
||||
* Display Related Logs in Grafana Metrics Drilldown
|
||||
*/
|
||||
exploreMetricsRelatedLogs?: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useId, memo, HTMLAttributes, ReactNode } from 'react';
|
||||
import { useId, memo, HTMLAttributes, ReactNode, SVGProps } from 'react';
|
||||
|
||||
import { FieldDisplay } from '@grafana/data';
|
||||
|
||||
@@ -50,14 +50,13 @@ export const RadialArcPath = memo(
|
||||
}: RadialArcPathProps) => {
|
||||
const id = useId();
|
||||
|
||||
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: '100%', height: '100%' };
|
||||
if ('color' in rest) {
|
||||
bgDivStyle.backgroundColor = rest.color;
|
||||
} else {
|
||||
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
|
||||
}
|
||||
const isGradient = 'gradient' in rest;
|
||||
|
||||
const { radius, centerX, centerY, barWidth } = dimensions;
|
||||
const { vizWidth, vizHeight, radius, centerX, centerY, barWidth } = dimensions;
|
||||
const pad = Math.ceil(Math.max(2, barWidth / 2)); // pad to cover stroke caps and glow in Safari
|
||||
const boxX = Math.round(centerX - radius - barWidth - pad);
|
||||
const boxY = Math.round(centerY - radius - barWidth - pad);
|
||||
const boxSize = Math.round((radius + barWidth) * 2 + pad * 2);
|
||||
|
||||
const path = drawRadialArcPath(angle, arcLengthDeg, dimensions, roundedBars);
|
||||
|
||||
@@ -72,9 +71,14 @@ export const RadialArcPath = memo(
|
||||
const dotRadius =
|
||||
endpointMarker === 'point' ? Math.min((barWidth / 2) * DOT_RADIUS_FACTOR, MAX_DOT_RADIUS) : barWidth / 2;
|
||||
|
||||
const bgDivStyle: HTMLAttributes<HTMLDivElement>['style'] = { width: boxSize, height: vizHeight, marginLeft: boxX };
|
||||
|
||||
const pathProps: SVGProps<SVGPathElement> = {};
|
||||
let barEndcapColors: [string, string] | undefined;
|
||||
let endpointMarks: ReactNode = null;
|
||||
if ('gradient' in rest) {
|
||||
if (isGradient) {
|
||||
bgDivStyle.backgroundImage = getGradientCss(rest.gradient, shape);
|
||||
|
||||
if (endpointMarker && (rest.gradient?.length ?? 0) > 0) {
|
||||
switch (endpointMarker) {
|
||||
case 'point':
|
||||
@@ -115,25 +119,39 @@ export const RadialArcPath = memo(
|
||||
if (barEndcaps) {
|
||||
barEndcapColors = getBarEndcapColors(rest.gradient, fieldDisplay.display.percent);
|
||||
}
|
||||
|
||||
pathProps.fill = 'none';
|
||||
pathProps.stroke = 'white';
|
||||
} else {
|
||||
bgDivStyle.backgroundColor = rest.color;
|
||||
|
||||
pathProps.fill = 'none';
|
||||
pathProps.stroke = rest.color;
|
||||
}
|
||||
|
||||
const pathEl = (
|
||||
<path d={path} strokeWidth={barWidth} strokeLinecap={roundedBars ? 'round' : 'butt'} {...pathProps} />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* FIXME: optimize this by only using clippath + foreign obj for gradients */}
|
||||
<clipPath id={id}>
|
||||
<path d={path} />
|
||||
</clipPath>
|
||||
{isGradient && (
|
||||
<defs>
|
||||
<mask id={id} maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse">
|
||||
<rect x={boxX} y={boxY} width={boxSize} height={boxSize} fill="black" />
|
||||
{pathEl}
|
||||
</mask>
|
||||
</defs>
|
||||
)}
|
||||
|
||||
<g filter={glowFilter}>
|
||||
<foreignObject
|
||||
x={centerX - radius - barWidth}
|
||||
y={centerY - radius - barWidth}
|
||||
width={(radius + barWidth) * 2}
|
||||
height={(radius + barWidth) * 2}
|
||||
clipPath={`url(#${id})`}
|
||||
>
|
||||
<div style={bgDivStyle} />
|
||||
</foreignObject>
|
||||
{isGradient ? (
|
||||
<foreignObject x={0} y={0} width={vizWidth} height={vizHeight} mask={`url(#${id})`}>
|
||||
<div style={bgDivStyle} />
|
||||
</foreignObject>
|
||||
) : (
|
||||
pathEl
|
||||
)}
|
||||
{barEndcapColors?.[0] && <circle cx={xStart} cy={yStart} r={barWidth / 2} fill={barEndcapColors[0]} />}
|
||||
{barEndcapColors?.[1] && (
|
||||
<circle cx={xEnd} cy={yEnd} r={barWidth / 2} fill={barEndcapColors[1]} opacity={0.5} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useId } from 'react';
|
||||
import { useId, ReactNode } from 'react';
|
||||
|
||||
import { DisplayValueAlignmentFactors, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@@ -107,14 +107,14 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
const startAngle = shape === 'gauge' ? 250 : 0;
|
||||
const endAngle = shape === 'gauge' ? 110 : 360;
|
||||
|
||||
const defs: React.ReactNode[] = [];
|
||||
const graphics: React.ReactNode[] = [];
|
||||
let sparklineElement: React.ReactNode | null = null;
|
||||
const defs: ReactNode[] = [];
|
||||
const graphics: ReactNode[] = [];
|
||||
let sparklineElement: ReactNode | null = null;
|
||||
|
||||
for (let barIndex = 0; barIndex < values.length; barIndex++) {
|
||||
const displayValue = values[barIndex];
|
||||
const { angle, angleRange } = getValueAngleForValue(displayValue, startAngle, endAngle);
|
||||
const gradientStops = buildGradientColors(gradient, theme, displayValue);
|
||||
const gradientStops = gradient ? buildGradientColors(theme, displayValue) : undefined;
|
||||
const color = displayValue.display.color ?? FALLBACK_COLOR;
|
||||
const dimensions = calculateDimensions(
|
||||
width,
|
||||
@@ -131,7 +131,9 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
// FIXME: I want to move the ids for these filters into a context which the children
|
||||
// can reference via a hook, rather than passing them down as props
|
||||
const spotlightGradientId = `spotlight-${barIndex}-${gaugeId}`;
|
||||
const spotlightGradientRef = endpointMarker === 'glow' ? `url(#${spotlightGradientId})` : undefined;
|
||||
const glowFilterId = `glow-${gaugeId}`;
|
||||
const glowFilterRef = glowBar ? `url(#${glowFilterId})` : undefined;
|
||||
|
||||
if (endpointMarker === 'glow') {
|
||||
defs.push(
|
||||
@@ -154,7 +156,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
fieldDisplay={displayValue}
|
||||
angleRange={angleRange}
|
||||
startAngle={startAngle}
|
||||
glowFilter={`url(#${glowFilterId})`}
|
||||
glowFilter={glowFilterRef}
|
||||
segmentCount={segmentCount}
|
||||
segmentSpacing={segmentSpacing}
|
||||
shape={shape}
|
||||
@@ -170,8 +172,8 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
angleRange={angleRange}
|
||||
startAngle={startAngle}
|
||||
roundedBars={roundedBars}
|
||||
glowFilter={`url(#${glowFilterId})`}
|
||||
endpointMarkerGlowFilter={`url(#${spotlightGradientId})`}
|
||||
glowFilter={glowFilterRef}
|
||||
endpointMarkerGlowFilter={spotlightGradientRef}
|
||||
shape={shape}
|
||||
gradient={gradientStops}
|
||||
fieldDisplay={displayValue}
|
||||
@@ -183,7 +185,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
// These elements are only added for first value / bar
|
||||
if (barIndex === 0) {
|
||||
if (glowBar) {
|
||||
defs.push(<GlowGradient key="glow-filter" id={glowFilterId} barWidth={dimensions.barWidth} />);
|
||||
defs.push(<GlowGradient key={glowFilterId} id={glowFilterId} barWidth={dimensions.barWidth} />);
|
||||
}
|
||||
|
||||
if (glowCenter) {
|
||||
@@ -234,7 +236,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
endAngle={endAngle}
|
||||
angleRange={angleRange}
|
||||
roundedBars={roundedBars}
|
||||
glowFilter={`url(#${glowFilterId})`}
|
||||
glowFilter={glowFilterRef}
|
||||
shape={shape}
|
||||
gradient={gradientStops}
|
||||
/>
|
||||
@@ -260,7 +262,7 @@ export function RadialGauge(props: RadialGaugeProps) {
|
||||
const body = (
|
||||
<>
|
||||
<svg width={width} height={height} role="img" aria-label={t('gauge.category-gauge', 'Gauge')}>
|
||||
<defs>{defs}</defs>
|
||||
{defs.length > 0 && <defs>{defs}</defs>}
|
||||
{graphics}
|
||||
</svg>
|
||||
{sparklineElement}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import {
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
GrafanaTheme2,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes/ThemeContext';
|
||||
import { calculateFontSize } from '../../utils/measureText';
|
||||
|
||||
import { RadialShape, RadialTextMode, RadialGaugeDimensions } from './types';
|
||||
@@ -50,7 +48,6 @@ export const RadialText = memo(
|
||||
valueManualFontSize,
|
||||
nameManualFontSize,
|
||||
}: RadialTextProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { centerX, centerY, radius, barWidth } = dimensions;
|
||||
|
||||
if (textMode === 'none') {
|
||||
@@ -106,10 +103,9 @@ export const RadialText = memo(
|
||||
const valueY = showName ? centerY - nameHeight * (1 - VALUE_SPACE_PERCENTAGE) : centerY;
|
||||
const nameY = showValue ? valueY + valueHeight * VALUE_SPACE_PERCENTAGE : centerY;
|
||||
const nameColor = showValue ? theme.colors.text.secondary : theme.colors.text.primary;
|
||||
const suffixShift = (valueFontSize - unitFontSize * LINE_HEIGHT_FACTOR) / 2;
|
||||
|
||||
// adjust the text up on gauges and when sparklines are present
|
||||
let yOffset = 0;
|
||||
let yOffset = valueFontSize / 4;
|
||||
if (shape === 'gauge') {
|
||||
// we render from the center of the gauge, so move up by half of half of the total height
|
||||
yOffset -= (valueHeight + nameHeight) / 4;
|
||||
@@ -126,15 +122,12 @@ export const RadialText = memo(
|
||||
y={valueY}
|
||||
fontSize={valueFontSize}
|
||||
fill={theme.colors.text.primary}
|
||||
className={styles.text}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
dominantBaseline="text-bottom"
|
||||
>
|
||||
<tspan fontSize={unitFontSize}>{displayValue.prefix ?? ''}</tspan>
|
||||
<tspan>{displayValue.text}</tspan>
|
||||
<tspan className={styles.text} fontSize={unitFontSize} dy={suffixShift}>
|
||||
{displayValue.suffix ?? ''}
|
||||
</tspan>
|
||||
<tspan fontSize={unitFontSize}>{displayValue.suffix ?? ''}</tspan>
|
||||
</text>
|
||||
)}
|
||||
{showName && (
|
||||
@@ -143,7 +136,7 @@ export const RadialText = memo(
|
||||
x={centerX}
|
||||
y={nameY}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
dominantBaseline="text-bottom"
|
||||
fill={nameColor}
|
||||
>
|
||||
{displayValue.title}
|
||||
@@ -155,9 +148,3 @@ export const RadialText = memo(
|
||||
);
|
||||
|
||||
RadialText.displayName = 'RadialText';
|
||||
|
||||
const getStyles = (_theme: GrafanaTheme2) => ({
|
||||
text: css({
|
||||
verticalAlign: 'bottom',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for center x and y 1`] = `"M 150 110 A 90 90 0 1 1 149.98429203681178 110.00000137077838 A 10 10 0 0 1 149.98778269529805 130.00000106616096 A 70 70 0 1 0 150 130 A 10 10 0 0 1 150 110 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for center x and y 1`] = `"M 150 120 A 80 80 0 1 1 149.98603736605492 120.00000121846968"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for half arc 1`] = `"M 100 10 A 90 90 0 0 1 100 190 L 100 170 A 70 70 0 0 0 100 30 L 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for half arc 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow bar width 1`] = `"M 100 17.5 A 82.5 82.5 0 0 1 100 182.5 L 100 177.5 A 77.5 77.5 0 0 0 100 22.5 L 100 17.5 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow bar width 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow radius 1`] = `"M 100 40 A 60 60 0 0 1 100 160 L 100 140 A 40 40 0 0 0 100 60 L 100 40 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for narrow radius 1`] = `"M 100 50 A 50 50 0 0 1 100 150"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for quarter arc 1`] = `"M 100 10 A 90 90 0 0 1 190 100 L 170 100 A 70 70 0 0 0 100 30 L 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for quarter arc 1`] = `"M 100 20 A 80 80 0 0 1 180 100"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for rounded bars 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 A 10 10 0 0 1 30 100.00000000000001 A 70 70 0 1 0 100 30 A 10 10 0 0 1 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for rounded bars 1`] = `"M 100 20 A 80 80 0 1 1 20 100.00000000000001"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for three quarter arc 1`] = `"M 100 10 A 90 90 0 1 1 10 100.00000000000001 L 30 100.00000000000001 A 70 70 0 1 0 100 30 L 100 10 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for three quarter arc 1`] = `"M 100 20 A 80 80 0 1 1 20 100.00000000000001"`;
|
||||
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 -5 A 105 105 0 0 1 100 205 L 100 155 A 55 55 0 0 0 100 45 L 100 -5 Z"`;
|
||||
exports[`RadialGauge utils drawRadialArcPath should draw correct path for wide bar width 1`] = `"M 100 20 A 80 80 0 0 1 100 180"`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defaultsDeep } from 'lodash';
|
||||
|
||||
import { createTheme, FALLBACK_COLOR, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data';
|
||||
import { createTheme, Field, FieldDisplay, FieldType, ThresholdsMode } from '@grafana/data';
|
||||
import { FieldColorModeId } from '@grafana/schema';
|
||||
|
||||
import {
|
||||
@@ -50,35 +50,9 @@ describe('RadialGauge color utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
it('should return the baseColor if gradient is false-y', () => {
|
||||
expect(
|
||||
buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000')
|
||||
).toEqual([
|
||||
{ color: '#FF0000', percent: 0 },
|
||||
{ color: '#FF0000', percent: 1 },
|
||||
]);
|
||||
|
||||
expect(
|
||||
buildGradientColors(undefined, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)), '#FF0000')
|
||||
).toEqual([
|
||||
{ color: '#FF0000', percent: 0 },
|
||||
{ color: '#FF0000', percent: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the fallback color if no baseColor is set', () => {
|
||||
expect(buildGradientColors(false, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Fixed)))).toEqual(
|
||||
[
|
||||
{ color: FALLBACK_COLOR, percent: 0 },
|
||||
{ color: FALLBACK_COLOR, percent: 1 },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme(),
|
||||
buildFieldDisplay(createField(FieldColorModeId.Thresholds), {
|
||||
view: { getFieldDisplayProcessor: jest.fn(() => jest.fn(() => ({ color: '#444444' }))) },
|
||||
@@ -89,14 +63,13 @@ describe('RadialGauge color utils', () => {
|
||||
|
||||
it('should map threshold colors correctly (with baseColor if displayProcessor does not return colors)', () => {
|
||||
expect(
|
||||
buildGradientColors(true, createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000')
|
||||
buildGradientColors(createTheme(), buildFieldDisplay(createField(FieldColorModeId.Thresholds)), '#FF0000')
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return gradient colors for continuous color modes', () => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme(),
|
||||
buildFieldDisplay(createField(FieldColorModeId.ContinuousCividis)),
|
||||
'#00FF00'
|
||||
@@ -107,7 +80,6 @@ describe('RadialGauge color utils', () => {
|
||||
it.each(['dark', 'light'] as const)('should return gradient colors for by-value color mode in %s theme', (mode) => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme({ colors: { mode } }),
|
||||
buildFieldDisplay(createField(FieldColorModeId.ContinuousBlues))
|
||||
)
|
||||
@@ -117,7 +89,6 @@ describe('RadialGauge color utils', () => {
|
||||
it.each(['dark', 'light'] as const)('should return gradient colors for fixed color mode in %s theme', (mode) => {
|
||||
expect(
|
||||
buildGradientColors(
|
||||
true,
|
||||
createTheme({ colors: { mode } }),
|
||||
buildFieldDisplay(createField(FieldColorModeId.Fixed)),
|
||||
'#442299'
|
||||
|
||||
@@ -7,18 +7,10 @@ import { GradientStop, RadialShape } from './types';
|
||||
import { getFieldConfigMinMax, getFieldDisplayProcessor, getValuePercentageForValue } from './utils';
|
||||
|
||||
export function buildGradientColors(
|
||||
gradient = false,
|
||||
theme: GrafanaTheme2,
|
||||
fieldDisplay: FieldDisplay,
|
||||
baseColor = fieldDisplay.display.color ?? FALLBACK_COLOR
|
||||
): GradientStop[] {
|
||||
if (!gradient) {
|
||||
return [
|
||||
{ color: baseColor, percent: 0 },
|
||||
{ color: baseColor, percent: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
const colorMode = getFieldColorMode(fieldDisplay.field.color?.mode);
|
||||
|
||||
// thresholds get special handling
|
||||
|
||||
@@ -2,14 +2,20 @@ import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { RadialGaugeDimensions } from './types';
|
||||
|
||||
// some utility transparent white colors for gradients
|
||||
const TRANSPARENT_WHITE = '#ffffff00';
|
||||
const MOSTLY_TRANSPARENT_WHITE = '#ffffff88';
|
||||
const MOSTLY_OPAQUE_WHITE = '#ffffffbb';
|
||||
const OPAQUE_WHITE = '#ffffff';
|
||||
|
||||
const MIN_GLOW_SIZE = 0.75;
|
||||
const GLOW_FACTOR = 0.08;
|
||||
|
||||
export interface GlowGradientProps {
|
||||
id: string;
|
||||
barWidth: number;
|
||||
}
|
||||
|
||||
const MIN_GLOW_SIZE = 0.75;
|
||||
const GLOW_FACTOR = 0.08;
|
||||
|
||||
export function GlowGradient({ id, barWidth }: GlowGradientProps) {
|
||||
// 0.75 is the minimum glow size, and it scales with bar width
|
||||
const glowSize = MIN_GLOW_SIZE + barWidth * GLOW_FACTOR;
|
||||
@@ -27,16 +33,6 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) {
|
||||
|
||||
const CENTER_GLOW_OPACITY = 0.25;
|
||||
|
||||
export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) {
|
||||
const transparentColor = colorManipulator.alpha(color, CENTER_GLOW_OPACITY);
|
||||
return (
|
||||
<radialGradient id={`circle-glow-${gaugeId}`} r="50%" fr="0%">
|
||||
<stop offset="0%" stopColor={transparentColor} />
|
||||
<stop offset="90%" stopColor={'#ffffff00'} />
|
||||
</radialGradient>
|
||||
);
|
||||
}
|
||||
|
||||
export interface CenterGlowProps {
|
||||
dimensions: RadialGaugeDimensions;
|
||||
gaugeId: string;
|
||||
@@ -52,7 +48,7 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps
|
||||
<defs>
|
||||
<radialGradient id={gradientId} r="50%" fr="0%">
|
||||
<stop offset="0%" stopColor={transparentColor} />
|
||||
<stop offset="90%" stopColor="#ffffff00" />
|
||||
<stop offset="90%" stopColor={TRANSPARENT_WHITE} />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g>
|
||||
@@ -62,19 +58,15 @@ export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps
|
||||
);
|
||||
}
|
||||
|
||||
export function SpotlightGradient({
|
||||
id,
|
||||
dimensions,
|
||||
roundedBars,
|
||||
angle,
|
||||
theme,
|
||||
}: {
|
||||
interface SpotlightGradientProps {
|
||||
id: string;
|
||||
dimensions: RadialGaugeDimensions;
|
||||
angle: number;
|
||||
roundedBars: boolean;
|
||||
theme: GrafanaTheme2;
|
||||
}) {
|
||||
}
|
||||
|
||||
export function SpotlightGradient({ id, dimensions, roundedBars, angle, theme }: SpotlightGradientProps) {
|
||||
if (theme.isLight) {
|
||||
return null;
|
||||
}
|
||||
@@ -88,9 +80,9 @@ export function SpotlightGradient({
|
||||
|
||||
return (
|
||||
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stopColor="#ffffff00" />
|
||||
<stop offset="95%" stopColor="#ffffff88" />
|
||||
{roundedBars && <stop offset="100%" stopColor={roundedBars ? '#ffffffbb' : 'white'} />}
|
||||
<stop offset="0%" stopColor={TRANSPARENT_WHITE} />
|
||||
<stop offset="95%" stopColor={MOSTLY_TRANSPARENT_WHITE} />
|
||||
{roundedBars && <stop offset="100%" stopColor={roundedBars ? MOSTLY_OPAQUE_WHITE : OPAQUE_WHITE} />}
|
||||
</linearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ export type RadialTextMode = 'auto' | 'value_and_name' | 'value' | 'name' | 'non
|
||||
export type RadialShape = 'circle' | 'gauge';
|
||||
|
||||
export interface RadialGaugeDimensions {
|
||||
vizHeight: number;
|
||||
vizWidth: number;
|
||||
margin: number;
|
||||
radius: number;
|
||||
centerX: number;
|
||||
|
||||
@@ -283,7 +283,9 @@ describe('RadialGauge utils', () => {
|
||||
});
|
||||
|
||||
describe('drawRadialArcPath', () => {
|
||||
const defaultDims: RadialGaugeDimensions = Object.freeze({
|
||||
const defaultDims = Object.freeze({
|
||||
vizHeight: 220,
|
||||
vizWidth: 220,
|
||||
centerX: 100,
|
||||
centerY: 100,
|
||||
radius: 80,
|
||||
@@ -297,7 +299,7 @@ describe('RadialGauge utils', () => {
|
||||
scaleLabelsSpacing: 0,
|
||||
scaleLabelsRadius: 0,
|
||||
gaugeBottomY: 0,
|
||||
});
|
||||
}) satisfies RadialGaugeDimensions;
|
||||
|
||||
it.each([
|
||||
{ description: 'quarter arc', startAngle: 0, endAngle: 90 },
|
||||
@@ -324,11 +326,6 @@ describe('RadialGauge utils', () => {
|
||||
expect(drawRadialArcPath(0, 360, defaultDims)).toEqual(drawRadialArcPath(0, 359.99, defaultDims));
|
||||
expect(drawRadialArcPath(0, 380, defaultDims)).toEqual(drawRadialArcPath(0, 380, defaultDims));
|
||||
});
|
||||
|
||||
it('should return empty string if inner radius collapses to zero or below', () => {
|
||||
const smallRadiusDims = { ...defaultDims, radius: 5, barWidth: 20 };
|
||||
expect(drawRadialArcPath(0, 180, smallRadiusDims)).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -341,7 +338,9 @@ describe('RadialGauge utils', () => {
|
||||
|
||||
describe('getOptimalSegmentCount', () => {
|
||||
it('should adjust segment count based on dimensions and spacing', () => {
|
||||
const dimensions: RadialGaugeDimensions = {
|
||||
const dimensions = {
|
||||
vizHeight: 220,
|
||||
vizWidth: 220,
|
||||
centerX: 100,
|
||||
centerY: 100,
|
||||
radius: 80,
|
||||
@@ -355,7 +354,7 @@ describe('RadialGauge utils', () => {
|
||||
scaleLabelsSpacing: 0,
|
||||
scaleLabelsRadius: 0,
|
||||
gaugeBottomY: 0,
|
||||
};
|
||||
} satisfies RadialGaugeDimensions;
|
||||
|
||||
expect(getOptimalSegmentCount(dimensions, 2, 10, 360)).toBe(8);
|
||||
expect(getOptimalSegmentCount(dimensions, 1, 5, 360)).toBe(5);
|
||||
|
||||
@@ -155,6 +155,8 @@ export function calculateDimensions(
|
||||
}
|
||||
|
||||
return {
|
||||
vizWidth: width,
|
||||
vizHeight: height,
|
||||
margin,
|
||||
gaugeBottomY: centerY + belowCenterY,
|
||||
radius: innerRadius,
|
||||
@@ -185,7 +187,7 @@ export function drawRadialArcPath(
|
||||
dimensions: RadialGaugeDimensions,
|
||||
roundedBars?: boolean
|
||||
): string {
|
||||
const { radius, centerX, centerY, barWidth } = dimensions;
|
||||
const { radius, centerX, centerY } = dimensions;
|
||||
|
||||
// For some reason a 100% full arc cannot be rendered
|
||||
if (endAngle >= 360) {
|
||||
@@ -197,66 +199,12 @@ export function drawRadialArcPath(
|
||||
|
||||
const largeArc = endAngle > 180 ? 1 : 0;
|
||||
|
||||
const outerR = radius + barWidth / 2;
|
||||
const innerR = Math.max(0, radius - barWidth / 2);
|
||||
if (innerR <= 0) {
|
||||
return ''; // cannot draw arc with 0 inner radius
|
||||
}
|
||||
let x1 = centerX + radius * Math.cos(startRadians);
|
||||
let y1 = centerY + radius * Math.sin(startRadians);
|
||||
let x2 = centerX + radius * Math.cos(endRadians);
|
||||
let y2 = centerY + radius * Math.sin(endRadians);
|
||||
|
||||
// get points for both an inner and outer arc. we draw
|
||||
// the arc entirely with a path's fill instead of using stroke
|
||||
// so that it can be used as a clip-path.
|
||||
const ox1 = centerX + outerR * Math.cos(startRadians);
|
||||
const oy1 = centerY + outerR * Math.sin(startRadians);
|
||||
const ox2 = centerX + outerR * Math.cos(endRadians);
|
||||
const oy2 = centerY + outerR * Math.sin(endRadians);
|
||||
|
||||
const ix1 = centerX + innerR * Math.cos(startRadians);
|
||||
const iy1 = centerY + innerR * Math.sin(startRadians);
|
||||
const ix2 = centerX + innerR * Math.cos(endRadians);
|
||||
const iy2 = centerY + innerR * Math.sin(endRadians);
|
||||
|
||||
// calculate the cap width in case we're drawing rounded bars
|
||||
const capR = barWidth / 2;
|
||||
|
||||
const pathParts = [
|
||||
// start at outer start
|
||||
'M',
|
||||
ox1,
|
||||
oy1,
|
||||
// outer arc from start to end (clockwise)
|
||||
'A',
|
||||
outerR,
|
||||
outerR,
|
||||
0,
|
||||
largeArc,
|
||||
1,
|
||||
ox2,
|
||||
oy2,
|
||||
];
|
||||
|
||||
if (roundedBars) {
|
||||
// rounded end cap: small arc connecting outer end to inner end
|
||||
pathParts.push('A', capR, capR, 0, 0, 1, ix2, iy2);
|
||||
} else {
|
||||
// straight line to inner end (square butt)
|
||||
pathParts.push('L', ix2, iy2);
|
||||
}
|
||||
|
||||
// inner arc from end back to start (counter-clockwise)
|
||||
pathParts.push('A', innerR, innerR, 0, largeArc, 0, ix1, iy1);
|
||||
|
||||
if (roundedBars) {
|
||||
// rounded start cap: small arc connecting inner start back to outer start
|
||||
pathParts.push('A', capR, capR, 0, 0, 1, ox1, oy1);
|
||||
} else {
|
||||
// straight line back to outer start (square butt)
|
||||
pathParts.push('L', ox1, oy1);
|
||||
}
|
||||
|
||||
pathParts.push('Z');
|
||||
|
||||
return pathParts.join(' ');
|
||||
return ['M', x1, y1, 'A', radius, radius, 0, largeArc, 1, x2, y2].join(' ');
|
||||
}
|
||||
|
||||
export function getAngleBetweenSegments(segmentSpacing: number, segmentCount: number, range: number) {
|
||||
|
||||
@@ -1108,12 +1108,18 @@ export function parseStyleJson(rawValue: unknown): CSSProperties | void {
|
||||
}
|
||||
}
|
||||
|
||||
// Safari 26 introduced rendering bugs which require us to disable several features of the table.
|
||||
// Safari 26.0 introduced rendering bugs which require us to disable several features of the table.
|
||||
// The bugs were later fixed in Safari 26.2.
|
||||
export const IS_SAFARI_26 = (() => {
|
||||
if (navigator == null) {
|
||||
return false;
|
||||
}
|
||||
const userAgent = navigator.userAgent;
|
||||
const safariVersionMatch = userAgent.match(/Version\/(\d+)\./);
|
||||
return safariVersionMatch && parseInt(safariVersionMatch[1], 10) === 26;
|
||||
const safariVersionMatch = userAgent.match(/Version\/(\d+)\.(\d+)/);
|
||||
if (!safariVersionMatch) {
|
||||
return false;
|
||||
}
|
||||
const majorVersion = +safariVersionMatch[1];
|
||||
const minorVersion = +safariVersionMatch[2];
|
||||
return majorVersion === 26 && minorVersion <= 1;
|
||||
})();
|
||||
|
||||
@@ -66,6 +66,6 @@ export interface UserView {
|
||||
avatarUrl?: string;
|
||||
};
|
||||
/** Datetime string when the user was last active */
|
||||
lastActiveAt: DateTimeInput;
|
||||
lastActiveAt?: DateTimeInput;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Tooltip } from '../Tooltip/Tooltip';
|
||||
import { UserView } from './types';
|
||||
|
||||
export interface UserIconProps {
|
||||
/** An object that contains the user's details and 'lastActiveAt' status */
|
||||
/** An object that contains the user's details and an optional 'lastActiveAt' status */
|
||||
userView: UserView;
|
||||
/** A boolean value that determines whether the tooltip should be shown or not */
|
||||
showTooltip?: boolean;
|
||||
@@ -64,7 +64,8 @@ export const UserIcon = ({
|
||||
showTooltip = true,
|
||||
}: PropsWithChildren<UserIconProps>) => {
|
||||
const { user, lastActiveAt } = userView;
|
||||
const isActive = dateTime(lastActiveAt).diff(dateTime(), 'minutes', true) >= -15;
|
||||
const hasActive = lastActiveAt !== undefined && lastActiveAt !== null;
|
||||
const isActive = hasActive && dateTime(lastActiveAt).diff(dateTime(), 'minutes', true) >= -15;
|
||||
const theme = useTheme2();
|
||||
const styles = useMemo(() => getStyles(theme, isActive), [theme, isActive]);
|
||||
const content = (
|
||||
@@ -88,18 +89,20 @@ export const UserIcon = ({
|
||||
const tooltip = (
|
||||
<div className={styles.tooltipContainer}>
|
||||
<div className={styles.tooltipName}>{user.name}</div>
|
||||
<div className={styles.tooltipDate}>
|
||||
{isActive ? (
|
||||
<div className={styles.dotContainer}>
|
||||
<span>
|
||||
<Trans i18nKey="grafana-ui.user-icon.active-text">Active last 15m</Trans>
|
||||
</span>
|
||||
<span className={styles.dot}></span>
|
||||
</div>
|
||||
) : (
|
||||
formatViewed(lastActiveAt)
|
||||
)}
|
||||
</div>
|
||||
{hasActive && (
|
||||
<div className={styles.tooltipDate}>
|
||||
{isActive ? (
|
||||
<div className={styles.dotContainer}>
|
||||
<span>
|
||||
<Trans i18nKey="grafana-ui.user-icon.active-text">Active last 15m</Trans>
|
||||
</span>
|
||||
<span className={styles.dot}></span>
|
||||
</div>
|
||||
) : (
|
||||
formatViewed(lastActiveAt)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -60,6 +60,6 @@ export interface UserView {
|
||||
avatarUrl?: string;
|
||||
};
|
||||
/** Datetime string when the user was last active */
|
||||
lastActiveAt: DateTimeInput;
|
||||
lastActiveAt?: DateTimeInput;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UserIcon } from './UserIcon';
|
||||
import { UserView } from './types';
|
||||
|
||||
export interface UsersIndicatorProps {
|
||||
/** An object that contains the user's details and 'lastActiveAt' status */
|
||||
/** An object that contains the user's details and an optional 'lastActiveAt' status */
|
||||
users: UserView[];
|
||||
/** A limit of how many user icons to show before collapsing them and showing a number of users instead */
|
||||
limit?: number;
|
||||
@@ -40,7 +40,7 @@ export const UsersIndicator = ({ users, onClick, limit = 4 }: UsersIndicatorProp
|
||||
aria-label={t('grafana-ui.users-indicator.container-label', 'Users indicator container')}
|
||||
>
|
||||
{limitReached && (
|
||||
<UserIcon onClick={onClick} userView={{ user: { name: 'Extra users' }, lastActiveAt: '' }} showTooltip={false}>
|
||||
<UserIcon onClick={onClick} userView={{ user: { name: 'Extra users' } }} showTooltip={false}>
|
||||
{tooManyUsers
|
||||
? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'...'
|
||||
|
||||
@@ -8,5 +8,5 @@ export interface UserView {
|
||||
avatarUrl?: string;
|
||||
};
|
||||
/** Datetime string when the user was last active */
|
||||
lastActiveAt: DateTimeInput;
|
||||
lastActiveAt?: DateTimeInput;
|
||||
}
|
||||
|
||||
@@ -226,8 +226,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Post("/api/user/email/start-verify", reqSignedInNoAnonymous, routing.Wrap(hs.StartEmailVerificaton))
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabledGlobally(featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
r.Post("/api/login/passwordless/start", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), hs.StartPasswordless)
|
||||
r.Post("/api/login/passwordless/authenticate", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPasswordless))
|
||||
}
|
||||
|
||||
@@ -410,8 +410,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
DisableSignoutMenu: hs.Cfg.DisableSignoutMenu,
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
hasEnabledProviders := hs.samlEnabled() || hs.authnService.IsClientEnabled(authn.ClientLDAP)
|
||||
|
||||
if !hasEnabledProviders {
|
||||
|
||||
44
pkg/services/accesscontrol/acimpl/basic_role_db_seed.go
Normal file
44
pkg/services/accesscontrol/acimpl/basic_role_db_seed.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package acimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
const (
|
||||
ossBasicRoleSeedLockName = "oss-ac-basic-role-seeder"
|
||||
ossBasicRoleSeedTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
// refreshBasicRolePermissionsInDB ensures basic role permissions are fully derived from in-memory registrations
|
||||
func (s *Service) refreshBasicRolePermissionsInDB(ctx context.Context, rolesSnapshot map[string][]accesscontrol.Permission) error {
|
||||
if s.sql == nil || s.seeder == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
run := func(ctx context.Context) error {
|
||||
desired := map[accesscontrol.SeedPermission]struct{}{}
|
||||
for role, permissions := range rolesSnapshot {
|
||||
for _, permission := range permissions {
|
||||
desired[accesscontrol.SeedPermission{BuiltInRole: role, Action: permission.Action, Scope: permission.Scope}] = struct{}{}
|
||||
}
|
||||
}
|
||||
s.seeder.SetDesiredPermissions(desired)
|
||||
return s.seeder.Seed(ctx)
|
||||
}
|
||||
|
||||
if s.serverLock == nil {
|
||||
return run(ctx)
|
||||
}
|
||||
|
||||
var err error
|
||||
errLock := s.serverLock.LockExecuteAndRelease(ctx, ossBasicRoleSeedLockName, ossBasicRoleSeedTimeout, func(ctx context.Context) {
|
||||
err = run(ctx)
|
||||
})
|
||||
if errLock != nil {
|
||||
return errLock
|
||||
}
|
||||
return err
|
||||
}
|
||||
128
pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go
Normal file
128
pkg/services/accesscontrol/acimpl/basic_role_db_seed_test.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package acimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/database"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
func TestIntegration_OSSBasicRolePermissions_PersistAndRefreshOnRegisterFixedRoles(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
ctx := context.Background()
|
||||
sql := db.InitTestDB(t)
|
||||
store := database.ProvideService(sql)
|
||||
|
||||
svc := ProvideOSSService(
|
||||
setting.NewCfg(),
|
||||
store,
|
||||
&resourcepermissions.FakeActionSetSvc{},
|
||||
localcache.ProvideService(),
|
||||
featuremgmt.WithFeatures(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
sql,
|
||||
permreg.ProvidePermissionRegistry(),
|
||||
nil,
|
||||
)
|
||||
|
||||
require.NoError(t, svc.DeclareFixedRoles(accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:role",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "test:read", Scope: ""},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleViewer)},
|
||||
}))
|
||||
|
||||
require.NoError(t, svc.RegisterFixedRoles(ctx))
|
||||
|
||||
// verify permission is persisted to DB for basic:viewer
|
||||
require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var role accesscontrol.Role
|
||||
ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
var count int64
|
||||
count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
return nil
|
||||
}))
|
||||
|
||||
// ensure RegisterFixedRoles refreshes it back to defaults
|
||||
require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
ts := time.Now()
|
||||
var role accesscontrol.Role
|
||||
ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = sess.Exec("DELETE FROM permission WHERE role_id = ?", role.ID)
|
||||
require.NoError(t, err)
|
||||
p := accesscontrol.Permission{
|
||||
RoleID: role.ID,
|
||||
Action: "custom:keep",
|
||||
Scope: "",
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
}
|
||||
p.Kind, p.Attribute, p.Identifier = accesscontrol.SplitScope(p.Scope)
|
||||
_, err = sess.Table("permission").Insert(&p)
|
||||
return err
|
||||
}))
|
||||
|
||||
svc2 := ProvideOSSService(
|
||||
setting.NewCfg(),
|
||||
store,
|
||||
&resourcepermissions.FakeActionSetSvc{},
|
||||
localcache.ProvideService(),
|
||||
featuremgmt.WithFeatures(),
|
||||
tracing.InitializeTracerForTest(),
|
||||
sql,
|
||||
permreg.ProvidePermissionRegistry(),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, svc2.DeclareFixedRoles(accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: "fixed:test:role",
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{Action: "test:read", Scope: ""},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleViewer)},
|
||||
}))
|
||||
require.NoError(t, svc2.RegisterFixedRoles(ctx))
|
||||
|
||||
require.NoError(t, sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var role accesscontrol.Role
|
||||
ok, err := sess.Table("role").Where("uid = ?", accesscontrol.BasicRoleUIDPrefix+"viewer").Get(&role)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
var count int64
|
||||
count, err = sess.Table("permission").Where("role_id = ? AND action = ? AND scope = ?", role.ID, "test:read", "").Count()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), count)
|
||||
|
||||
count, err = sess.Table("permission").Where("role_id = ? AND action = ?", role.ID, "custom:keep").Count()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0), count)
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/seeding"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -96,6 +97,12 @@ func ProvideOSSService(
|
||||
roles: accesscontrol.BuildBasicRoleDefinitions(),
|
||||
store: store,
|
||||
permRegistry: permRegistry,
|
||||
sql: db,
|
||||
serverLock: lock,
|
||||
}
|
||||
|
||||
if backend, ok := store.(*database.AccessControlStore); ok {
|
||||
s.seeder = seeding.New(log.New("accesscontrol.seeder"), backend, backend)
|
||||
}
|
||||
|
||||
return s
|
||||
@@ -112,8 +119,11 @@ type Service struct {
|
||||
rolesMu sync.RWMutex
|
||||
roles map[string]*accesscontrol.RoleDTO
|
||||
store accesscontrol.Store
|
||||
seeder *seeding.Seeder
|
||||
permRegistry permreg.PermissionRegistry
|
||||
isInitialized bool
|
||||
sql db.DB
|
||||
serverLock *serverlock.ServerLockService
|
||||
}
|
||||
|
||||
func (s *Service) GetUsageStats(_ context.Context) map[string]any {
|
||||
@@ -431,17 +441,54 @@ func (s *Service) RegisterFixedRoles(ctx context.Context) error {
|
||||
defer span.End()
|
||||
|
||||
s.rolesMu.Lock()
|
||||
defer s.rolesMu.Unlock()
|
||||
|
||||
registrations := s.registrations.Slice()
|
||||
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
|
||||
s.registerRolesLocked(registration)
|
||||
return true
|
||||
})
|
||||
|
||||
s.isInitialized = true
|
||||
|
||||
rolesSnapshot := s.getBasicRolePermissionsLocked()
|
||||
s.rolesMu.Unlock()
|
||||
|
||||
if s.seeder != nil {
|
||||
if err := s.seeder.SeedRoles(ctx, registrations); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.seeder.RemoveAbsentRoles(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getBasicRolePermissionsSnapshotFromRegistrationsLocked computes the desired basic role permissions from the
|
||||
// current registration list, using the shared seeding registration logic.
|
||||
//
|
||||
// it has to be called while holding the roles lock
|
||||
func (s *Service) getBasicRolePermissionsLocked() map[string][]accesscontrol.Permission {
|
||||
desired := map[accesscontrol.SeedPermission]struct{}{}
|
||||
s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool {
|
||||
seeding.AppendDesiredPermissions(desired, s.log, ®istration.Role, registration.Grants, registration.Exclude)
|
||||
return true
|
||||
})
|
||||
|
||||
out := make(map[string][]accesscontrol.Permission)
|
||||
for sp := range desired {
|
||||
out[sp.BuiltInRole] = append(out[sp.BuiltInRole], accesscontrol.Permission{
|
||||
Action: sp.Action,
|
||||
Scope: sp.Scope,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// registerRolesLocked processes a single role registration and adds permissions to basic roles.
|
||||
// Must be called with s.rolesMu locked.
|
||||
func (s *Service) registerRolesLocked(registration accesscontrol.RoleRegistration) {
|
||||
@@ -474,6 +521,7 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
||||
defer span.End()
|
||||
|
||||
acRegs := pluginutils.ToRegistrations(ID, name, regs)
|
||||
updatedBasicRoles := false
|
||||
for _, r := range acRegs {
|
||||
if err := pluginutils.ValidatePluginRole(ID, r.Role); err != nil {
|
||||
return err
|
||||
@@ -500,11 +548,23 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
||||
if initialized {
|
||||
s.rolesMu.Lock()
|
||||
s.registerRolesLocked(r)
|
||||
updatedBasicRoles = true
|
||||
s.rolesMu.Unlock()
|
||||
s.cache.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
if updatedBasicRoles {
|
||||
s.rolesMu.RLock()
|
||||
rolesSnapshot := s.getBasicRolePermissionsLocked()
|
||||
s.rolesMu.RUnlock()
|
||||
|
||||
// plugin roles can be declared after startup - keep DB in sync
|
||||
if err := s.refreshBasicRolePermissionsInDB(ctx, rolesSnapshot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -167,99 +167,3 @@ func (c *LegacyAccessClient) Compile(ctx context.Context, id claims.AuthInfo, re
|
||||
return check(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, name))
|
||||
}, claims.NoopZookie{}, nil
|
||||
}
|
||||
|
||||
func (c *LegacyAccessClient) BatchCheck(ctx context.Context, id claims.AuthInfo, req claims.BatchCheckRequest) (claims.BatchCheckResponse, error) {
|
||||
ident, ok := id.(identity.Requester)
|
||||
if !ok {
|
||||
return claims.BatchCheckResponse{}, errors.New("expected identity.Requester for legacy access control")
|
||||
}
|
||||
|
||||
results := make(map[string]claims.BatchCheckResult, len(req.Checks))
|
||||
|
||||
// Cache checkers by action to avoid recreating them for each check
|
||||
checkerCache := make(map[string]func(scopes ...string) bool)
|
||||
|
||||
for _, check := range req.Checks {
|
||||
opts, ok := c.opts[check.Resource]
|
||||
if !ok {
|
||||
// For now w fallback to grafana admin if no options are found for resource.
|
||||
if ident.GetIsGrafanaAdmin() {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true}
|
||||
} else {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: false}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if verb should be skipped
|
||||
if opts.Unchecked[check.Verb] {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: true}
|
||||
continue
|
||||
}
|
||||
|
||||
action, ok := opts.Mapping[check.Verb]
|
||||
if !ok {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: fmt.Errorf("missing action for %s %s", check.Verb, check.Resource),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Get or create cached checker for this action
|
||||
checker, ok := checkerCache[action]
|
||||
if !ok {
|
||||
checker = Checker(ident, action)
|
||||
checkerCache[action] = checker
|
||||
}
|
||||
|
||||
// Handle list and create verbs (no specific name)
|
||||
// TODO: Should we allow list/create without name in a BatchCheck request?
|
||||
if check.Name == "" {
|
||||
if check.Verb == utils.VerbList || check.Verb == utils.VerbCreate {
|
||||
// For list/create without name, check if user has the action at all
|
||||
// TODO: Is this correct for Create?
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: len(ident.GetPermissions()[action]) > 0,
|
||||
}
|
||||
} else {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: fmt.Errorf("unhandled authorization: %s %s", check.Group, check.Verb),
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check with resolver or direct scope
|
||||
var allowed bool
|
||||
if opts.Resolver != nil {
|
||||
ns, err := claims.ParseNamespace(check.Namespace)
|
||||
if err != nil {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
scopes, err := opts.Resolver.Resolve(ctx, ns, check.Name)
|
||||
if err != nil {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err,
|
||||
}
|
||||
continue
|
||||
}
|
||||
allowed = checker(scopes...)
|
||||
} else {
|
||||
allowed = checker(fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, check.Name))
|
||||
}
|
||||
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{Allowed: allowed}
|
||||
}
|
||||
|
||||
return claims.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: claims.NoopZookie{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -136,220 +136,6 @@ func TestLegacyAccessClient_Check(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLegacyAccessClient_BatchCheck(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
|
||||
t.Run("should return empty results for empty checks", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, res.Results)
|
||||
})
|
||||
|
||||
t.Run("should reject unknown resource for non-admin", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "unknown", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should allow unknown resource for grafana admin", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{IsGrafanaAdmin: true}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "unknown", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should allow unchecked verbs", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Unchecked: map[string]bool{"get": true},
|
||||
})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should return error for missing action mapping", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{}, // Empty mapping
|
||||
})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), &identity.StaticRequester{}, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, res.Results["check-1"].Allowed)
|
||||
assert.Error(t, res.Results["check-1"].Error)
|
||||
})
|
||||
|
||||
t.Run("should allow when user has correct scope", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:1"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should reject when user has wrong scope", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:2"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should handle list without name", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"list": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:*"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "list", Resource: "dashboards", Name: ""},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should handle multiple checks with mixed results", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
})
|
||||
|
||||
ident := newIdent(
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:1"},
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:3"},
|
||||
)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
{CorrelationID: "check-2", Verb: "get", Resource: "dashboards", Name: "2"},
|
||||
{CorrelationID: "check-3", Verb: "get", Resource: "dashboards", Name: "3"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
assert.False(t, res.Results["check-2"].Allowed)
|
||||
assert.True(t, res.Results["check-3"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should use resolver when provided", func(t *testing.T) {
|
||||
resolver := accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns authlib.NamespaceInfo, name string) ([]string, error) {
|
||||
// Resolve dashboard name to folder scope
|
||||
return []string{"folders:uid:folder-a"}, nil
|
||||
})
|
||||
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read"},
|
||||
Resolver: resolver,
|
||||
})
|
||||
|
||||
ident := newIdent(accesscontrol.Permission{Action: "dashboards:read", Scope: "folders:uid:folder-a"})
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "check-1", Verb: "get", Resource: "dashboards", Name: "1", Namespace: "default"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.Results["check-1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should cache checker by action", func(t *testing.T) {
|
||||
a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{
|
||||
Resource: "dashboards",
|
||||
Attr: "uid",
|
||||
Mapping: map[string]string{"get": "dashboards:read", "update": "dashboards:write"},
|
||||
})
|
||||
|
||||
ident := newIdent(
|
||||
accesscontrol.Permission{Action: "dashboards:read", Scope: "dashboards:uid:*"},
|
||||
accesscontrol.Permission{Action: "dashboards:write", Scope: "dashboards:uid:1"},
|
||||
)
|
||||
|
||||
res, err := a.BatchCheck(context.Background(), ident, authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{CorrelationID: "read-1", Verb: "get", Resource: "dashboards", Name: "1"},
|
||||
{CorrelationID: "read-2", Verb: "get", Resource: "dashboards", Name: "2"},
|
||||
{CorrelationID: "write-1", Verb: "update", Resource: "dashboards", Name: "1"},
|
||||
{CorrelationID: "write-2", Verb: "update", Resource: "dashboards", Name: "2"},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
// Read with wildcard scope should allow all
|
||||
assert.True(t, res.Results["read-1"].Allowed)
|
||||
assert.True(t, res.Results["read-2"].Allowed)
|
||||
// Write only has scope for uid:1
|
||||
assert.True(t, res.Results["write-1"].Allowed)
|
||||
assert.False(t, res.Results["write-2"].Allowed)
|
||||
})
|
||||
}
|
||||
|
||||
func newIdent(permissions ...accesscontrol.Permission) *identity.StaticRequester {
|
||||
pmap := map[string][]string{}
|
||||
for _, p := range permissions {
|
||||
|
||||
623
pkg/services/accesscontrol/database/seeder.go
Normal file
623
pkg/services/accesscontrol/database/seeder.go
Normal file
@@ -0,0 +1,623 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/seeding"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/util/xorm/core"
|
||||
)
|
||||
|
||||
const basicRolePermBatchSize = 500
|
||||
|
||||
// LoadRoles returns all fixed and plugin roles (global org) with permissions, indexed by role name.
|
||||
func (s *AccessControlStore) LoadRoles(ctx context.Context) (map[string]*accesscontrol.RoleDTO, error) {
|
||||
out := map[string]*accesscontrol.RoleDTO{}
|
||||
|
||||
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
type roleRow struct {
|
||||
ID int64 `xorm:"id"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
Version int64 `xorm:"version"`
|
||||
UID string `xorm:"uid"`
|
||||
Name string `xorm:"name"`
|
||||
DisplayName string `xorm:"display_name"`
|
||||
Description string `xorm:"description"`
|
||||
Group string `xorm:"group_name"`
|
||||
Hidden bool `xorm:"hidden"`
|
||||
Updated time.Time `xorm:"updated"`
|
||||
Created time.Time `xorm:"created"`
|
||||
}
|
||||
|
||||
roles := []roleRow{}
|
||||
if err := sess.Table("role").
|
||||
Where("org_id = ?", accesscontrol.GlobalOrgID).
|
||||
Where("(name LIKE ? OR name LIKE ?)", accesscontrol.FixedRolePrefix+"%", accesscontrol.PluginRolePrefix+"%").
|
||||
Find(&roles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(roles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
roleIDs := make([]any, 0, len(roles))
|
||||
roleByID := make(map[int64]*accesscontrol.RoleDTO, len(roles))
|
||||
for _, r := range roles {
|
||||
dto := &accesscontrol.RoleDTO{
|
||||
ID: r.ID,
|
||||
OrgID: r.OrgID,
|
||||
Version: r.Version,
|
||||
UID: r.UID,
|
||||
Name: r.Name,
|
||||
DisplayName: r.DisplayName,
|
||||
Description: r.Description,
|
||||
Group: r.Group,
|
||||
Hidden: r.Hidden,
|
||||
Updated: r.Updated,
|
||||
Created: r.Created,
|
||||
}
|
||||
out[dto.Name] = dto
|
||||
roleByID[dto.ID] = dto
|
||||
roleIDs = append(roleIDs, dto.ID)
|
||||
}
|
||||
|
||||
type permRow struct {
|
||||
RoleID int64 `xorm:"role_id"`
|
||||
Action string `xorm:"action"`
|
||||
Scope string `xorm:"scope"`
|
||||
}
|
||||
perms := []permRow{}
|
||||
if err := sess.Table("permission").In("role_id", roleIDs...).Find(&perms); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range perms {
|
||||
dto := roleByID[p.RoleID]
|
||||
if dto == nil {
|
||||
continue
|
||||
}
|
||||
dto.Permissions = append(dto.Permissions, accesscontrol.Permission{
|
||||
RoleID: p.RoleID,
|
||||
Action: p.Action,
|
||||
Scope: p.Scope,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) SetRole(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error {
|
||||
if existingRole == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
_, err := sess.Table("role").
|
||||
Where("id = ? AND org_id = ?", existingRole.ID, accesscontrol.GlobalOrgID).
|
||||
Update(map[string]any{
|
||||
"display_name": wantedRole.DisplayName,
|
||||
"description": wantedRole.Description,
|
||||
"group_name": wantedRole.Group,
|
||||
"hidden": wantedRole.Hidden,
|
||||
"updated": time.Now(),
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) SetPermissions(ctx context.Context, existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) error {
|
||||
if existingRole == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
type key struct{ Action, Scope string }
|
||||
existing := map[key]struct{}{}
|
||||
for _, p := range existingRole.Permissions {
|
||||
existing[key{p.Action, p.Scope}] = struct{}{}
|
||||
}
|
||||
desired := map[key]struct{}{}
|
||||
for _, p := range wantedRole.Permissions {
|
||||
desired[key{p.Action, p.Scope}] = struct{}{}
|
||||
}
|
||||
|
||||
toAdd := make([]accesscontrol.Permission, 0)
|
||||
toRemove := make([]accesscontrol.SeedPermission, 0)
|
||||
|
||||
now := time.Now()
|
||||
for k := range desired {
|
||||
if _, ok := existing[k]; ok {
|
||||
continue
|
||||
}
|
||||
perm := accesscontrol.Permission{
|
||||
RoleID: existingRole.ID,
|
||||
Action: k.Action,
|
||||
Scope: k.Scope,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope)
|
||||
toAdd = append(toAdd, perm)
|
||||
}
|
||||
|
||||
for k := range existing {
|
||||
if _, ok := desired[k]; ok {
|
||||
continue
|
||||
}
|
||||
toRemove = append(toRemove, accesscontrol.SeedPermission{Action: k.Action, Scope: k.Scope})
|
||||
}
|
||||
|
||||
if len(toAdd) == 0 && len(toRemove) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
if len(toRemove) > 0 {
|
||||
if err := DeleteRolePermissionTuples(sess, s.sql.GetDBType(), existingRole.ID, toRemove); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(toAdd) > 0 {
|
||||
_, err := sess.InsertMulti(toAdd)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) CreateRole(ctx context.Context, role accesscontrol.RoleDTO) error {
|
||||
now := time.Now()
|
||||
uid := role.UID
|
||||
if uid == "" && (strings.HasPrefix(role.Name, accesscontrol.FixedRolePrefix) || strings.HasPrefix(role.Name, accesscontrol.PluginRolePrefix)) {
|
||||
uid = accesscontrol.PrefixedRoleUID(role.Name)
|
||||
}
|
||||
r := accesscontrol.Role{
|
||||
OrgID: accesscontrol.GlobalOrgID,
|
||||
Version: role.Version,
|
||||
UID: uid,
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
Description: role.Description,
|
||||
Group: role.Group,
|
||||
Hidden: role.Hidden,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
if r.Version == 0 {
|
||||
r.Version = 1
|
||||
}
|
||||
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
if _, err := sess.Insert(&r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(role.Permissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// De-duplicate permissions on (action, scope) to avoid unique constraint violations.
|
||||
// Some role definitions may accidentally include duplicates.
|
||||
type permKey struct{ Action, Scope string }
|
||||
seen := make(map[permKey]struct{}, len(role.Permissions))
|
||||
|
||||
perms := make([]accesscontrol.Permission, 0, len(role.Permissions))
|
||||
for _, p := range role.Permissions {
|
||||
k := permKey{Action: p.Action, Scope: p.Scope}
|
||||
if _, ok := seen[k]; ok {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
|
||||
perm := accesscontrol.Permission{
|
||||
RoleID: r.ID,
|
||||
Action: p.Action,
|
||||
Scope: p.Scope,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
}
|
||||
perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope)
|
||||
perms = append(perms, perm)
|
||||
}
|
||||
_, err := sess.InsertMulti(perms)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) DeleteRoles(ctx context.Context, roleUIDs []string) error {
|
||||
if len(roleUIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
uids := make([]any, 0, len(roleUIDs))
|
||||
for _, uid := range roleUIDs {
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
type row struct {
|
||||
ID int64 `xorm:"id"`
|
||||
UID string `xorm:"uid"`
|
||||
}
|
||||
rows := []row{}
|
||||
if err := sess.Table("role").
|
||||
Where("org_id = ?", accesscontrol.GlobalOrgID).
|
||||
In("uid", uids...).
|
||||
Find(&rows); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
roleIDs := make([]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
roleIDs = append(roleIDs, r.ID)
|
||||
}
|
||||
|
||||
// Remove permissions and assignments first to avoid FK issues (if enabled).
|
||||
{
|
||||
args := append([]any{"DELETE FROM permission WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{
|
||||
args := append([]any{"DELETE FROM user_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{
|
||||
args := append([]any{"DELETE FROM team_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
{
|
||||
args := append([]any{"DELETE FROM builtin_role WHERE role_id IN (?" + strings.Repeat(",?", len(roleIDs)-1) + ")"}, roleIDs...)
|
||||
if _, err := sess.Exec(args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
args := append([]any{"DELETE FROM role WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")", accesscontrol.GlobalOrgID}, uids...)
|
||||
_, err := sess.Exec(args...)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// OSS basic-role permission refresh uses seeding.Seeder.Seed() with a desired set computed in memory.
|
||||
// These methods implement the permission seeding part of seeding.SeedingBackend against the current permission table.
|
||||
func (s *AccessControlStore) LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error) {
|
||||
var out map[accesscontrol.SeedPermission]struct{}
|
||||
err := s.sql.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
rows, err := LoadBasicRoleSeedPermissions(sess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out = make(map[accesscontrol.SeedPermission]struct{}, len(rows))
|
||||
for _, r := range rows {
|
||||
r.Origin = ""
|
||||
out[r] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) Apply(ctx context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error {
|
||||
rolesToUpgrade := seeding.RolesToUpgrade(added, removed)
|
||||
|
||||
// Run the same OSS apply logic as ossBasicRoleSeedBackend.Apply inside a single transaction.
|
||||
return s.sql.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
defs := accesscontrol.BuildBasicRoleDefinitions()
|
||||
builtinToRoleID, err := EnsureBasicRolesExist(sess, defs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backend := &ossBasicRoleSeedBackend{
|
||||
sess: sess,
|
||||
now: time.Now(),
|
||||
builtinToRoleID: builtinToRoleID,
|
||||
desired: nil,
|
||||
dbType: s.sql.GetDBType(),
|
||||
}
|
||||
if err := backend.Apply(ctx, added, removed, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return BumpBasicRoleVersions(sess, rolesToUpgrade)
|
||||
})
|
||||
}
|
||||
|
||||
// EnsureBasicRolesExist ensures the built-in basic roles exist in the role table and are bound in builtin_role.
|
||||
// It returns a mapping from builtin role name (for example "Admin") to role ID.
|
||||
func EnsureBasicRolesExist(sess *db.Session, defs map[string]*accesscontrol.RoleDTO) (map[string]int64, error) {
|
||||
uidToBuiltin := make(map[string]string, len(defs))
|
||||
uids := make([]any, 0, len(defs))
|
||||
for builtin, def := range defs {
|
||||
uidToBuiltin[def.UID] = builtin
|
||||
uids = append(uids, def.UID)
|
||||
}
|
||||
|
||||
type roleRow struct {
|
||||
ID int64 `xorm:"id"`
|
||||
UID string `xorm:"uid"`
|
||||
}
|
||||
|
||||
rows := []roleRow{}
|
||||
if err := sess.Table("role").
|
||||
Where("org_id = ?", accesscontrol.GlobalOrgID).
|
||||
In("uid", uids...).
|
||||
Find(&rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts := time.Now()
|
||||
|
||||
builtinToRoleID := make(map[string]int64, len(defs))
|
||||
for _, r := range rows {
|
||||
br, ok := uidToBuiltin[r.UID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
builtinToRoleID[br] = r.ID
|
||||
}
|
||||
|
||||
for builtin, def := range defs {
|
||||
roleID, ok := builtinToRoleID[builtin]
|
||||
if !ok {
|
||||
role := accesscontrol.Role{
|
||||
OrgID: def.OrgID,
|
||||
Version: def.Version,
|
||||
UID: def.UID,
|
||||
Name: def.Name,
|
||||
DisplayName: def.DisplayName,
|
||||
Description: def.Description,
|
||||
Group: def.Group,
|
||||
Hidden: def.Hidden,
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
}
|
||||
if _, err := sess.Insert(&role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roleID = role.ID
|
||||
builtinToRoleID[builtin] = roleID
|
||||
}
|
||||
|
||||
has, err := sess.Table("builtin_role").
|
||||
Where("role_id = ? AND role = ? AND org_id = ?", roleID, builtin, accesscontrol.GlobalOrgID).
|
||||
Exist()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
br := accesscontrol.BuiltinRole{
|
||||
RoleID: roleID,
|
||||
OrgID: accesscontrol.GlobalOrgID,
|
||||
Role: builtin,
|
||||
Created: ts,
|
||||
Updated: ts,
|
||||
}
|
||||
if _, err := sess.Table("builtin_role").Insert(&br); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builtinToRoleID, nil
|
||||
}
|
||||
|
||||
// DeleteRolePermissionTuples deletes permissions for a single role by (action, scope) pairs.
|
||||
//
|
||||
// It uses a row-constructor IN clause where supported (MySQL, Postgres, SQLite) and falls back
|
||||
// to a WHERE ... OR ... form for MSSQL.
|
||||
func DeleteRolePermissionTuples(sess *db.Session, dbType core.DbType, roleID int64, perms []accesscontrol.SeedPermission) error {
|
||||
if len(perms) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dbType == migrator.MSSQL {
|
||||
// MSSQL doesn't support (action, scope) IN ((?,?),(?,?)) row constructors.
|
||||
where := make([]string, 0, len(perms))
|
||||
args := make([]any, 0, 1+len(perms)*2)
|
||||
args = append(args, roleID)
|
||||
for _, p := range perms {
|
||||
where = append(where, "(action = ? AND scope = ?)")
|
||||
args = append(args, p.Action, p.Scope)
|
||||
}
|
||||
_, err := sess.Exec(
|
||||
append([]any{
|
||||
"DELETE FROM permission WHERE role_id = ? AND (" + strings.Join(where, " OR ") + ")",
|
||||
}, args...)...,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
args := make([]any, 0, 1+len(perms)*2)
|
||||
args = append(args, roleID)
|
||||
for _, p := range perms {
|
||||
args = append(args, p.Action, p.Scope)
|
||||
}
|
||||
sql := "DELETE FROM permission WHERE role_id = ? AND (action, scope) IN (" +
|
||||
strings.Repeat("(?, ?),", len(perms)-1) + "(?, ?))"
|
||||
_, err := sess.Exec(append([]any{sql}, args...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
type ossBasicRoleSeedBackend struct {
|
||||
sess *db.Session
|
||||
now time.Time
|
||||
builtinToRoleID map[string]int64
|
||||
desired map[accesscontrol.SeedPermission]struct{}
|
||||
dbType core.DbType
|
||||
}
|
||||
|
||||
func (b *ossBasicRoleSeedBackend) LoadPrevious(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) {
|
||||
rows, err := LoadBasicRoleSeedPermissions(b.sess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[accesscontrol.SeedPermission]struct{}, len(rows))
|
||||
for _, r := range rows {
|
||||
// Ensure the key matches what OSS seeding uses (Origin is always empty for basic role refresh).
|
||||
r.Origin = ""
|
||||
out[r] = struct{}{}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (b *ossBasicRoleSeedBackend) LoadDesired(_ context.Context) (map[accesscontrol.SeedPermission]struct{}, error) {
|
||||
return b.desired, nil
|
||||
}
|
||||
|
||||
func (b *ossBasicRoleSeedBackend) Apply(_ context.Context, added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) error {
|
||||
// Delete removed permissions (this includes user-defined permissions that aren't in desired).
|
||||
if len(removed) > 0 {
|
||||
permsByRoleID := map[int64][]accesscontrol.SeedPermission{}
|
||||
for _, p := range removed {
|
||||
roleID, ok := b.builtinToRoleID[p.BuiltInRole]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
permsByRoleID[roleID] = append(permsByRoleID[roleID], p)
|
||||
}
|
||||
|
||||
for roleID, perms := range permsByRoleID {
|
||||
// Chunk to keep statement sizes and parameter counts bounded.
|
||||
if err := batch(len(perms), basicRolePermBatchSize, func(start, end int) error {
|
||||
return DeleteRolePermissionTuples(b.sess, b.dbType, roleID, perms[start:end])
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert added permissions and updated-target permissions.
|
||||
toInsertSeed := make([]accesscontrol.SeedPermission, 0, len(added)+len(updated))
|
||||
toInsertSeed = append(toInsertSeed, added...)
|
||||
for _, v := range updated {
|
||||
toInsertSeed = append(toInsertSeed, v)
|
||||
}
|
||||
if len(toInsertSeed) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// De-duplicate on (role_id, action, scope). This avoids unique constraint violations when:
|
||||
// - the same permission appears in both added and updated
|
||||
// - multiple plugin origins grant the same permission (Origin is not persisted in permission table)
|
||||
type permKey struct {
|
||||
RoleID int64
|
||||
Action string
|
||||
Scope string
|
||||
}
|
||||
seen := make(map[permKey]struct{}, len(toInsertSeed))
|
||||
|
||||
toInsert := make([]accesscontrol.Permission, 0, len(toInsertSeed))
|
||||
for _, p := range toInsertSeed {
|
||||
roleID, ok := b.builtinToRoleID[p.BuiltInRole]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
k := permKey{RoleID: roleID, Action: p.Action, Scope: p.Scope}
|
||||
if _, ok := seen[k]; ok {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
|
||||
perm := accesscontrol.Permission{
|
||||
RoleID: roleID,
|
||||
Action: p.Action,
|
||||
Scope: p.Scope,
|
||||
Created: b.now,
|
||||
Updated: b.now,
|
||||
}
|
||||
perm.Kind, perm.Attribute, perm.Identifier = accesscontrol.SplitScope(perm.Scope)
|
||||
toInsert = append(toInsert, perm)
|
||||
}
|
||||
|
||||
return batch(len(toInsert), basicRolePermBatchSize, func(start, end int) error {
|
||||
// MySQL: ignore conflicts to make seeding idempotent under retries/concurrency.
|
||||
// Conflicts can happen if the same permission already exists (unique on role_id, action, scope).
|
||||
if b.dbType == migrator.MySQL {
|
||||
args := make([]any, 0, (end-start)*8)
|
||||
for i := start; i < end; i++ {
|
||||
p := toInsert[i]
|
||||
args = append(args, p.RoleID, p.Action, p.Scope, p.Kind, p.Attribute, p.Identifier, p.Updated, p.Created)
|
||||
}
|
||||
sql := append([]any{`INSERT IGNORE INTO permission (role_id, action, scope, kind, attribute, identifier, updated, created) VALUES ` +
|
||||
strings.Repeat("(?, ?, ?, ?, ?, ?, ?, ?),", end-start-1) + "(?, ?, ?, ?, ?, ?, ?, ?)"}, args...)
|
||||
_, err := b.sess.Exec(sql...)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := b.sess.InsertMulti(toInsert[start:end])
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func batch(count, size int, eachFn func(start, end int) error) error {
|
||||
for i := 0; i < count; {
|
||||
end := i + size
|
||||
if end > count {
|
||||
end = count
|
||||
}
|
||||
if err := eachFn(i, end); err != nil {
|
||||
return err
|
||||
}
|
||||
i = end
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BumpBasicRoleVersions increments the role version for the given builtin basic roles (Viewer/Editor/Admin/Grafana Admin).
|
||||
// Unknown role names are ignored.
|
||||
func BumpBasicRoleVersions(sess *db.Session, basicRoles []string) error {
|
||||
if len(basicRoles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
defs := accesscontrol.BuildBasicRoleDefinitions()
|
||||
uids := make([]any, 0, len(basicRoles))
|
||||
for _, br := range basicRoles {
|
||||
def, ok := defs[br]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
uids = append(uids, def.UID)
|
||||
}
|
||||
if len(uids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sql := "UPDATE role SET version = version + 1 WHERE org_id = ? AND uid IN (?" + strings.Repeat(",?", len(uids)-1) + ")"
|
||||
_, err := sess.Exec(append([]any{sql, accesscontrol.GlobalOrgID}, uids...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadBasicRoleSeedPermissions returns the current (builtin_role, action, scope) permissions granted to basic roles.
|
||||
// It sets Origin to empty.
|
||||
func LoadBasicRoleSeedPermissions(sess *db.Session) ([]accesscontrol.SeedPermission, error) {
|
||||
rows := []accesscontrol.SeedPermission{}
|
||||
err := sess.SQL(
|
||||
`SELECT role.display_name AS builtin_role, p.action, p.scope, '' AS origin
|
||||
FROM role INNER JOIN permission AS p ON p.role_id = role.id
|
||||
WHERE role.org_id = ? AND role.name LIKE 'basic:%'`,
|
||||
accesscontrol.GlobalOrgID,
|
||||
).Find(&rows)
|
||||
return rows, err
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/serverlock"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -130,6 +131,9 @@ func (r *ZanzanaReconciler) Run(ctx context.Context) error {
|
||||
// Reconcile schedules as job that will run and reconcile resources between
|
||||
// legacy access control and zanzana.
|
||||
func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
|
||||
// Ensure we don't reconcile an empty/partial RBAC state before OSS has seeded basic role permissions.
|
||||
// This matters most during startup where fixed-role loading + basic-role permission refresh runs as another background service.
|
||||
r.waitForBasicRolesSeeded(ctx)
|
||||
r.reconcile(ctx)
|
||||
|
||||
// FIXME:
|
||||
@@ -145,6 +149,57 @@ func (r *ZanzanaReconciler) Reconcile(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ZanzanaReconciler) hasBasicRolePermissions(ctx context.Context) bool {
|
||||
var count int64
|
||||
// Basic role permissions are stored on "basic:%" roles in the global org (0).
|
||||
// In a fresh DB, this will be empty until fixed roles are registered and the basic role permission refresh runs.
|
||||
type row struct {
|
||||
Count int64 `xorm:"count"`
|
||||
}
|
||||
_ = r.store.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
var rr row
|
||||
_, err := sess.SQL(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM role INNER JOIN permission AS p ON p.role_id = role.id
|
||||
WHERE role.org_id = ? AND role.name LIKE ?`,
|
||||
accesscontrol.GlobalOrgID,
|
||||
accesscontrol.BasicRolePrefix+"%",
|
||||
).Get(&rr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count = rr.Count
|
||||
return nil
|
||||
})
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (r *ZanzanaReconciler) waitForBasicRolesSeeded(ctx context.Context) {
|
||||
// Best-effort: don't block forever. If we can't observe basic roles, proceed anyway.
|
||||
const (
|
||||
maxWait = 15 * time.Second
|
||||
interval = 1 * time.Second
|
||||
)
|
||||
|
||||
deadline := time.NewTimer(maxWait)
|
||||
defer deadline.Stop()
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
if r.hasBasicRolePermissions(ctx) {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-deadline.C:
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ZanzanaReconciler) reconcile(ctx context.Context) {
|
||||
run := func(ctx context.Context, namespace string) (ok bool) {
|
||||
now := time.Now()
|
||||
|
||||
67
pkg/services/accesscontrol/dualwrite/reconciler_test.go
Normal file
67
pkg/services/accesscontrol/dualwrite/reconciler_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package dualwrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
func TestZanzanaReconciler_hasBasicRolePermissions(t *testing.T) {
|
||||
env := setupTestEnv(t)
|
||||
|
||||
r := &ZanzanaReconciler{
|
||||
store: env.db,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
require.False(t, r.hasBasicRolePermissions(ctx))
|
||||
|
||||
err := env.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := sess.Exec(
|
||||
`INSERT INTO role (org_id, uid, name, display_name, group_name, description, hidden, version, created, updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
accesscontrol.GlobalOrgID,
|
||||
"basic_viewer_uid_test",
|
||||
accesscontrol.BasicRolePrefix+"viewer",
|
||||
"Viewer",
|
||||
"Basic",
|
||||
"Viewer role",
|
||||
false,
|
||||
1,
|
||||
now,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var roleID int64
|
||||
if _, err := sess.SQL(`SELECT id FROM role WHERE org_id = ? AND uid = ?`, accesscontrol.GlobalOrgID, "basic_viewer_uid_test").Get(&roleID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sess.Exec(
|
||||
`INSERT INTO permission (role_id, action, scope, kind, attribute, identifier, created, updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
roleID,
|
||||
"dashboards:read",
|
||||
"dashboards:*",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
now,
|
||||
now,
|
||||
)
|
||||
return err
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, r.hasBasicRolePermissions(ctx))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package accesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -594,3 +595,18 @@ type QueryWithOrg struct {
|
||||
OrgId *int64 `json:"orgId"`
|
||||
Global bool `json:"global"`
|
||||
}
|
||||
|
||||
type SeedPermission struct {
|
||||
BuiltInRole string `xorm:"builtin_role"`
|
||||
Action string `xorm:"action"`
|
||||
Scope string `xorm:"scope"`
|
||||
Origin string `xorm:"origin"`
|
||||
}
|
||||
|
||||
type RoleStore interface {
|
||||
LoadRoles(ctx context.Context) (map[string]*RoleDTO, error)
|
||||
SetRole(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error
|
||||
SetPermissions(ctx context.Context, existingRole *RoleDTO, wantedRole RoleDTO) error
|
||||
CreateRole(ctx context.Context, role RoleDTO) error
|
||||
DeleteRoles(ctx context.Context, roleUIDs []string) error
|
||||
}
|
||||
|
||||
452
pkg/services/accesscontrol/seeding/seeder.go
Normal file
452
pkg/services/accesscontrol/seeding/seeder.go
Normal file
@@ -0,0 +1,452 @@
|
||||
package seeding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
)
|
||||
|
||||
type Seeder struct {
|
||||
log log.Logger
|
||||
roleStore accesscontrol.RoleStore
|
||||
backend SeedingBackend
|
||||
builtinsPermissions map[accesscontrol.SeedPermission]struct{}
|
||||
seededFixedRoles map[string]bool
|
||||
seededPluginRoles map[string]bool
|
||||
seededPlugins map[string]bool
|
||||
hasSeededAlready bool
|
||||
}
|
||||
|
||||
// SeedingBackend provides the seed-set specific operations needed to seed.
|
||||
type SeedingBackend interface {
|
||||
// LoadPrevious returns the currently stored permissions for previously seeded roles.
|
||||
LoadPrevious(ctx context.Context) (map[accesscontrol.SeedPermission]struct{}, error)
|
||||
|
||||
// Apply updates the database to match the desired permissions.
|
||||
Apply(ctx context.Context,
|
||||
added, removed []accesscontrol.SeedPermission,
|
||||
updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission,
|
||||
) error
|
||||
}
|
||||
|
||||
func New(log log.Logger, roleStore accesscontrol.RoleStore, backend SeedingBackend) *Seeder {
|
||||
return &Seeder{
|
||||
log: log,
|
||||
roleStore: roleStore,
|
||||
backend: backend,
|
||||
builtinsPermissions: map[accesscontrol.SeedPermission]struct{}{},
|
||||
seededFixedRoles: map[string]bool{},
|
||||
seededPluginRoles: map[string]bool{},
|
||||
seededPlugins: map[string]bool{},
|
||||
hasSeededAlready: false,
|
||||
}
|
||||
}
|
||||
|
||||
// SetDesiredPermissions replaces the in-memory desired permission set used by Seed().
|
||||
func (s *Seeder) SetDesiredPermissions(desired map[accesscontrol.SeedPermission]struct{}) {
|
||||
if desired == nil {
|
||||
s.builtinsPermissions = map[accesscontrol.SeedPermission]struct{}{}
|
||||
return
|
||||
}
|
||||
s.builtinsPermissions = desired
|
||||
}
|
||||
|
||||
// Seed loads current and desired permissions, diffs them (including scope updates), applies changes, and bumps versions.
|
||||
func (s *Seeder) Seed(ctx context.Context) error {
|
||||
previous, err := s.backend.LoadPrevious(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// - Do not remove plugin permissions when the plugin didn't register this run (Origin set but not in seededPlugins).
|
||||
// - Preserve legacy plugin app access permissions in the persisted seed set (these are granted by default).
|
||||
if len(previous) > 0 {
|
||||
filtered := make(map[accesscontrol.SeedPermission]struct{}, len(previous))
|
||||
for p := range previous {
|
||||
// Legacy plugin app access permissions (Origin set) are granted by default and managed outside seeding.
|
||||
// Keep them out of the diff so seeding doesn't try to remove or "re-add" them on every run.
|
||||
if p.Action == pluginaccesscontrol.ActionAppAccess && p.Origin != "" {
|
||||
continue
|
||||
}
|
||||
if p.Origin != "" && !s.seededPlugins[p.Origin] {
|
||||
continue
|
||||
}
|
||||
filtered[p] = struct{}{}
|
||||
}
|
||||
previous = filtered
|
||||
}
|
||||
|
||||
added, removed, updated := s.permissionDiff(previous, s.builtinsPermissions)
|
||||
|
||||
if err := s.backend.Apply(ctx, added, removed, updated); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedRoles populates the database with the roles and their assignments
|
||||
// It will create roles that do not exist and update roles that have changed
|
||||
// Do not use for provisioning. Validation is not enforced.
|
||||
func (s *Seeder) SeedRoles(ctx context.Context, registrationList []accesscontrol.RoleRegistration) error {
|
||||
roleMap, err := s.roleStore.LoadRoles(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
missingRoles := make([]accesscontrol.RoleRegistration, 0, len(registrationList))
|
||||
|
||||
// Diff existing roles with the ones we want to seed.
|
||||
// If a role is missing, we add it to the missingRoles list
|
||||
for _, registration := range registrationList {
|
||||
registration := registration
|
||||
role, ok := roleMap[registration.Role.Name]
|
||||
switch {
|
||||
case registration.Role.IsFixed():
|
||||
s.seededFixedRoles[registration.Role.Name] = true
|
||||
case registration.Role.IsPlugin():
|
||||
s.seededPluginRoles[registration.Role.Name] = true
|
||||
// To be resilient to failed plugin loadings, we remember the plugins that have registered,
|
||||
// later we'll ignore permissions and roles of other plugins
|
||||
s.seededPlugins[pluginutils.PluginIDFromName(registration.Role.Name)] = true
|
||||
}
|
||||
|
||||
s.rememberPermissionAssignments(®istration.Role, registration.Grants, registration.Exclude)
|
||||
|
||||
if !ok {
|
||||
missingRoles = append(missingRoles, registration)
|
||||
continue
|
||||
}
|
||||
|
||||
if needsRoleUpdate(role, registration.Role) {
|
||||
if err := s.roleStore.SetRole(ctx, role, registration.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if needsPermissionsUpdate(role, registration.Role) {
|
||||
if err := s.roleStore.SetPermissions(ctx, role, registration.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, registration := range missingRoles {
|
||||
if err := s.roleStore.CreateRole(ctx, registration.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func needsPermissionsUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool {
|
||||
if existingRole == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(existingRole.Permissions) != len(wantedRole.Permissions) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, p := range wantedRole.Permissions {
|
||||
found := false
|
||||
for _, ep := range existingRole.Permissions {
|
||||
if ep.Action == p.Action && ep.Scope == p.Scope {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func needsRoleUpdate(existingRole *accesscontrol.RoleDTO, wantedRole accesscontrol.RoleDTO) bool {
|
||||
if existingRole == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Name != wantedRole.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if existingRole.DisplayName != wantedRole.DisplayName {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Description != wantedRole.Description {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Group != wantedRole.Group {
|
||||
return true
|
||||
}
|
||||
|
||||
if existingRole.Hidden != wantedRole.Hidden {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Deprecated: SeedRole is deprecated and should not be used.
|
||||
// SeedRoles only does boot up seeding and should not be used for runtime seeding.
|
||||
func (s *Seeder) SeedRole(ctx context.Context, role accesscontrol.RoleDTO, builtInRoles []string) error {
|
||||
addedPermissions := make(map[string]struct{}, len(role.Permissions))
|
||||
permissions := make([]accesscontrol.Permission, 0, len(role.Permissions))
|
||||
for _, p := range role.Permissions {
|
||||
key := fmt.Sprintf("%s:%s", p.Action, p.Scope)
|
||||
if _, ok := addedPermissions[key]; !ok {
|
||||
addedPermissions[key] = struct{}{}
|
||||
permissions = append(permissions, accesscontrol.Permission{Action: p.Action, Scope: p.Scope})
|
||||
}
|
||||
}
|
||||
|
||||
wantedRole := accesscontrol.RoleDTO{
|
||||
OrgID: accesscontrol.GlobalOrgID,
|
||||
Version: role.Version,
|
||||
UID: role.UID,
|
||||
Name: role.Name,
|
||||
DisplayName: role.DisplayName,
|
||||
Description: role.Description,
|
||||
Group: role.Group,
|
||||
Permissions: permissions,
|
||||
Hidden: role.Hidden,
|
||||
}
|
||||
roleMap, err := s.roleStore.LoadRoles(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingRole := roleMap[wantedRole.Name]
|
||||
if existingRole == nil {
|
||||
if err := s.roleStore.CreateRole(ctx, wantedRole); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if needsRoleUpdate(existingRole, wantedRole) {
|
||||
if err := s.roleStore.SetRole(ctx, existingRole, wantedRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if needsPermissionsUpdate(existingRole, wantedRole) {
|
||||
if err := s.roleStore.SetPermissions(ctx, existingRole, wantedRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remember seeded roles
|
||||
if wantedRole.IsFixed() {
|
||||
s.seededFixedRoles[wantedRole.Name] = true
|
||||
}
|
||||
isPluginRole := wantedRole.IsPlugin()
|
||||
if isPluginRole {
|
||||
s.seededPluginRoles[wantedRole.Name] = true
|
||||
|
||||
// To be resilient to failed plugin loadings, we remember the plugins that have registered,
|
||||
// later we'll ignore permissions and roles of other plugins
|
||||
s.seededPlugins[pluginutils.PluginIDFromName(role.Name)] = true
|
||||
}
|
||||
|
||||
s.rememberPermissionAssignments(&wantedRole, builtInRoles, []string{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Seeder) rememberPermissionAssignments(role *accesscontrol.RoleDTO, builtInRoles []string, excludedRoles []string) {
|
||||
AppendDesiredPermissions(s.builtinsPermissions, s.log, role, builtInRoles, excludedRoles)
|
||||
}
|
||||
|
||||
// AppendDesiredPermissions accumulates permissions from a role registration onto basic roles (Viewer/Editor/Admin/Grafana Admin).
|
||||
// - It expands parents via accesscontrol.BuiltInRolesWithParents.
|
||||
// - It can optionally ignore plugin app access permissions (which are granted by default).
|
||||
func AppendDesiredPermissions(
|
||||
out map[accesscontrol.SeedPermission]struct{},
|
||||
logger log.Logger,
|
||||
role *accesscontrol.RoleDTO,
|
||||
builtInRoles []string,
|
||||
excludedRoles []string,
|
||||
) {
|
||||
if out == nil || role == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for builtInRole := range accesscontrol.BuiltInRolesWithParents(builtInRoles) {
|
||||
// Skip excluded grants
|
||||
if slices.Contains(excludedRoles, builtInRole) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, perm := range role.Permissions {
|
||||
if role.IsPlugin() && perm.Action == pluginaccesscontrol.ActionAppAccess {
|
||||
logger.Debug("Role is attempting to grant access permission, but this permission is already granted by default and will be ignored",
|
||||
"role", role.Name, "permission", perm.Action, "scope", perm.Scope)
|
||||
continue
|
||||
}
|
||||
|
||||
sp := accesscontrol.SeedPermission{
|
||||
BuiltInRole: builtInRole,
|
||||
Action: perm.Action,
|
||||
Scope: perm.Scope,
|
||||
}
|
||||
|
||||
if role.IsPlugin() {
|
||||
sp.Origin = pluginutils.PluginIDFromName(role.Name)
|
||||
}
|
||||
|
||||
out[sp] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// permissionDiff returns:
|
||||
// - added: present in desired permissions, not in previous permissions
|
||||
// - removed: present in previous permissions, not in desired permissions
|
||||
// - updated: same role + action, but scope changed
|
||||
func (s *Seeder) permissionDiff(previous, desired map[accesscontrol.SeedPermission]struct{}) (added, removed []accesscontrol.SeedPermission, updated map[accesscontrol.SeedPermission]accesscontrol.SeedPermission) {
|
||||
addedSet := make(map[accesscontrol.SeedPermission]struct{}, 0)
|
||||
for n := range desired {
|
||||
if _, already := previous[n]; !already {
|
||||
addedSet[n] = struct{}{}
|
||||
} else {
|
||||
delete(previous, n)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any of the new permissions is actually an old permission with an updated scope
|
||||
updated = make(map[accesscontrol.SeedPermission]accesscontrol.SeedPermission, 0)
|
||||
for n := range addedSet {
|
||||
for p := range previous {
|
||||
if n.BuiltInRole == p.BuiltInRole && n.Action == p.Action {
|
||||
updated[p] = n
|
||||
delete(addedSet, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for p := range addedSet {
|
||||
added = append(added, p)
|
||||
}
|
||||
|
||||
for p := range previous {
|
||||
if p.Action == pluginaccesscontrol.ActionAppAccess &&
|
||||
p.Scope != pluginaccesscontrol.ScopeProvider.GetResourceAllScope() {
|
||||
// Allows backward compatibility with plugins that have been seeded before the grant ignore rule was added
|
||||
s.log.Info("This permission already existed so it will not be removed",
|
||||
"role", p.BuiltInRole, "permission", p.Action, "scope", p.Scope)
|
||||
continue
|
||||
}
|
||||
|
||||
removed = append(removed, p)
|
||||
}
|
||||
|
||||
return added, removed, updated
|
||||
}
|
||||
|
||||
func (s *Seeder) ClearBasicRolesPluginPermissions(ID string) {
|
||||
removable := []accesscontrol.SeedPermission{}
|
||||
|
||||
for key := range s.builtinsPermissions {
|
||||
if matchPermissionByPluginID(key, ID) {
|
||||
removable = append(removable, key)
|
||||
}
|
||||
}
|
||||
|
||||
for _, perm := range removable {
|
||||
delete(s.builtinsPermissions, perm)
|
||||
}
|
||||
}
|
||||
|
||||
func matchPermissionByPluginID(perm accesscontrol.SeedPermission, pluginID string) bool {
|
||||
if perm.Origin != pluginID {
|
||||
return false
|
||||
}
|
||||
actionTemplate := regexp.MustCompile(fmt.Sprintf("%s[.:]", pluginID))
|
||||
scopeTemplate := fmt.Sprintf(":%s", pluginID)
|
||||
return actionTemplate.MatchString(perm.Action) || strings.HasSuffix(perm.Scope, scopeTemplate)
|
||||
}
|
||||
|
||||
// RolesToUpgrade returns the unique basic roles that should have their version incremented.
|
||||
func RolesToUpgrade(added, removed []accesscontrol.SeedPermission) []string {
|
||||
set := map[string]struct{}{}
|
||||
for _, p := range added {
|
||||
set[p.BuiltInRole] = struct{}{}
|
||||
}
|
||||
for _, p := range removed {
|
||||
set[p.BuiltInRole] = struct{}{}
|
||||
}
|
||||
out := make([]string, 0, len(set))
|
||||
for r := range set {
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Seeder) ClearPluginRoles(ID string) {
|
||||
expectedPrefix := fmt.Sprintf("%s%s:", accesscontrol.PluginRolePrefix, ID)
|
||||
|
||||
for roleName := range s.seededPluginRoles {
|
||||
if strings.HasPrefix(roleName, expectedPrefix) {
|
||||
delete(s.seededPluginRoles, roleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Seeder) MarkSeededAlready() {
|
||||
s.hasSeededAlready = true
|
||||
}
|
||||
|
||||
func (s *Seeder) HasSeededAlready() bool {
|
||||
return s.hasSeededAlready
|
||||
}
|
||||
|
||||
func (s *Seeder) RemoveAbsentRoles(ctx context.Context) error {
|
||||
roleMap, errGet := s.roleStore.LoadRoles(ctx)
|
||||
if errGet != nil {
|
||||
s.log.Error("failed to get fixed roles from store", "err", errGet)
|
||||
return errGet
|
||||
}
|
||||
|
||||
toRemove := []string{}
|
||||
for _, r := range roleMap {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
if r.IsFixed() {
|
||||
if !s.seededFixedRoles[r.Name] {
|
||||
s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name)
|
||||
toRemove = append(toRemove, r.UID)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if r.IsPlugin() {
|
||||
if !s.seededPlugins[pluginutils.PluginIDFromName(r.Name)] {
|
||||
// To be resilient to failed plugin loadings
|
||||
// ignore stored roles related to plugins that have not registered this time
|
||||
s.log.Debug("plugin role has not been registered on this run skipping its removal", "role", r.Name)
|
||||
continue
|
||||
}
|
||||
if !s.seededPluginRoles[r.Name] {
|
||||
s.log.Info("role is not seeded anymore, mark it for deletion", "role", r.Name)
|
||||
toRemove = append(toRemove, r.UID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errDelete := s.roleStore.DeleteRoles(ctx, toRemove); errDelete != nil {
|
||||
s.log.Error("failed to delete absent fixed and plugin roles", "err", errDelete)
|
||||
return errDelete
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package authnimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -83,8 +81,7 @@ func ProvideRegistration(
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if cfg.PasswordlessMagicLinkAuth.Enabled && features.IsEnabled(context.Background(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
if cfg.PasswordlessMagicLinkAuth.Enabled {
|
||||
hasEnabledProviders := authnSvc.IsClientEnabled(authn.ClientSAML) || authnSvc.IsClientEnabled(authn.ClientLDAP)
|
||||
if !hasEnabledProviders {
|
||||
oauthInfos := socialService.GetOAuthInfoProviders()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@ import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
service AuthzExtentionService {
|
||||
rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse);
|
||||
|
||||
rpc Read(ReadRequest) returns (ReadResponse);
|
||||
rpc Write(WriteRequest) returns (WriteResponse);
|
||||
|
||||
@@ -229,6 +231,29 @@ message WriteRequest {
|
||||
|
||||
message WriteResponse {}
|
||||
|
||||
message BatchCheckRequest {
|
||||
string subject = 1;
|
||||
string namespace = 2;
|
||||
repeated BatchCheckItem items = 3;
|
||||
}
|
||||
|
||||
message BatchCheckItem {
|
||||
string verb = 1;
|
||||
string group = 2;
|
||||
string resource = 3;
|
||||
string name = 4;
|
||||
string subresource = 5;
|
||||
string folder = 6;
|
||||
}
|
||||
|
||||
message BatchCheckResponse {
|
||||
map<string, BatchCheckGroupResource> groups = 1;
|
||||
}
|
||||
|
||||
message BatchCheckGroupResource {
|
||||
map<string, bool> items = 1;
|
||||
}
|
||||
|
||||
message QueryRequest {
|
||||
string namespace = 1;
|
||||
QueryOperation operation = 2;
|
||||
|
||||
@@ -19,16 +19,18 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion8
|
||||
|
||||
const (
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate"
|
||||
AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query"
|
||||
AuthzExtentionService_BatchCheck_FullMethodName = "/authz.extention.v1.AuthzExtentionService/BatchCheck"
|
||||
AuthzExtentionService_Read_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Read"
|
||||
AuthzExtentionService_Write_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Write"
|
||||
AuthzExtentionService_Mutate_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Mutate"
|
||||
AuthzExtentionService_Query_FullMethodName = "/authz.extention.v1.AuthzExtentionService/Query"
|
||||
)
|
||||
|
||||
// AuthzExtentionServiceClient is the client API for AuthzExtentionService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type AuthzExtentionServiceClient interface {
|
||||
BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error)
|
||||
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
|
||||
Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error)
|
||||
Mutate(ctx context.Context, in *MutateRequest, opts ...grpc.CallOption) (*MutateResponse, error)
|
||||
@@ -43,6 +45,16 @@ func NewAuthzExtentionServiceClient(cc grpc.ClientConnInterface) AuthzExtentionS
|
||||
return &authzExtentionServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(BatchCheckResponse)
|
||||
err := c.cc.Invoke(ctx, AuthzExtentionService_BatchCheck_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *authzExtentionServiceClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReadResponse)
|
||||
@@ -87,6 +99,7 @@ func (c *authzExtentionServiceClient) Query(ctx context.Context, in *QueryReques
|
||||
// All implementations should embed UnimplementedAuthzExtentionServiceServer
|
||||
// for forward compatibility
|
||||
type AuthzExtentionServiceServer interface {
|
||||
BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error)
|
||||
Read(context.Context, *ReadRequest) (*ReadResponse, error)
|
||||
Write(context.Context, *WriteRequest) (*WriteResponse, error)
|
||||
Mutate(context.Context, *MutateRequest) (*MutateResponse, error)
|
||||
@@ -97,6 +110,9 @@ type AuthzExtentionServiceServer interface {
|
||||
type UnimplementedAuthzExtentionServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedAuthzExtentionServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchCheck not implemented")
|
||||
}
|
||||
func (UnimplementedAuthzExtentionServiceServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Read not implemented")
|
||||
}
|
||||
@@ -121,6 +137,24 @@ func RegisterAuthzExtentionServiceServer(s grpc.ServiceRegistrar, srv AuthzExten
|
||||
s.RegisterService(&AuthzExtentionService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BatchCheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AuthzExtentionService_BatchCheck_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthzExtentionServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AuthzExtentionService_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReadRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -200,6 +234,10 @@ var AuthzExtentionService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "authz.extention.v1.AuthzExtentionService",
|
||||
HandlerType: (*AuthzExtentionServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "BatchCheck",
|
||||
Handler: _AuthzExtentionService_BatchCheck_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Read",
|
||||
Handler: _AuthzExtentionService_Read_Handler,
|
||||
|
||||
@@ -186,150 +186,6 @@ func (s *Service) Check(ctx context.Context, req *authzv1.CheckRequest) (*authzv
|
||||
return &authzv1.CheckResponse{Allowed: allowed}, nil
|
||||
}
|
||||
|
||||
// BatchCheck implements authzv1.AuthzServiceServer.BatchCheck
|
||||
// This performs multiple access checks in a single request with optimized batching.
|
||||
// 1. Validates the subject once
|
||||
// 2. Groups checks by (namespace, action) to load permissions once per group
|
||||
// 3. Reuses the folder tree across checks
|
||||
func (s *Service) BatchCheck(ctx context.Context, req *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
checks := req.GetChecks()
|
||||
span.SetAttributes(attribute.Int("check_count", len(checks)))
|
||||
|
||||
ctxLogger := s.logger.FromContext(ctx).New(
|
||||
"subject", req.GetSubject(),
|
||||
"check_count", len(checks),
|
||||
)
|
||||
defer func(start time.Time) {
|
||||
ctxLogger.Debug("BatchCheck execution time", "duration", time.Since(start).Milliseconds())
|
||||
}(time.Now())
|
||||
|
||||
// Early check for auth info - required for namespace validation
|
||||
if _, has := types.AuthInfoFrom(ctx); !has {
|
||||
return nil, status.Error(codes.Internal, "could not get auth info from context")
|
||||
}
|
||||
|
||||
if len(checks) == 0 {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: make(map[string]*authzv1.BatchCheckResult),
|
||||
Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate subject once for all checks
|
||||
userUID, idType, err := s.validateSubject(ctx, req.GetSubject())
|
||||
if err != nil {
|
||||
ctxLogger.Error("invalid subject", "error", err)
|
||||
// Return all checks as denied with the same error
|
||||
results := make(map[string]*authzv1.BatchCheckResult, len(checks))
|
||||
for _, item := range checks {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{
|
||||
Allowed: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
return &authzv1.BatchCheckResponse{Results: results, Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()}}, nil
|
||||
}
|
||||
|
||||
results := make(map[string]*authzv1.BatchCheckResult, len(checks))
|
||||
|
||||
// Group checks by (namespace, action) to batch permission lookups
|
||||
type checkGroup struct {
|
||||
namespace types.NamespaceInfo
|
||||
action string
|
||||
actionSets []string
|
||||
items []*authzv1.BatchCheckItem
|
||||
checkReqs []*checkRequest
|
||||
}
|
||||
groups := make(map[string]*checkGroup)
|
||||
|
||||
// First pass: validate and group checks
|
||||
for _, item := range checks {
|
||||
ns, err := validateNamespace(ctx, item.GetNamespace())
|
||||
if err != nil {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
action, actionSets, err := s.validateAction(ctx, item.GetGroup(), item.GetResource(), item.GetVerb())
|
||||
if err != nil {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
// Create the internal check request
|
||||
checkReq := &checkRequest{
|
||||
Namespace: ns,
|
||||
UserUID: userUID,
|
||||
IdentityType: idType,
|
||||
Action: action,
|
||||
ActionSets: actionSets,
|
||||
Group: item.GetGroup(),
|
||||
Resource: item.GetResource(),
|
||||
Verb: item.GetVerb(),
|
||||
Name: item.GetName(),
|
||||
ParentFolder: item.GetFolder(),
|
||||
}
|
||||
|
||||
// Group by namespace + action
|
||||
groupKey := ns.Value + ":" + action
|
||||
if g, ok := groups[groupKey]; ok {
|
||||
g.items = append(g.items, item)
|
||||
g.checkReqs = append(g.checkReqs, checkReq)
|
||||
} else {
|
||||
groups[groupKey] = &checkGroup{
|
||||
namespace: ns,
|
||||
action: action,
|
||||
actionSets: actionSets,
|
||||
items: []*authzv1.BatchCheckItem{item},
|
||||
checkReqs: []*checkRequest{checkReq},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: process each group with shared permissions
|
||||
for _, group := range groups {
|
||||
// Set namespace in context for this group (required by store methods)
|
||||
groupCtx := request.WithNamespace(ctx, group.namespace.Value)
|
||||
|
||||
// Try to get cached permissions first, then fall back to store
|
||||
permissions, err := s.getCachedIdentityPermissions(groupCtx, group.namespace, idType, userUID, group.action)
|
||||
if err != nil {
|
||||
// Cache miss - fetch from store
|
||||
permissions, err = s.getIdentityPermissions(groupCtx, group.namespace, idType, userUID, group.action, group.actionSets)
|
||||
if err != nil {
|
||||
ctxLogger.Error("could not get permissions", "namespace", group.namespace.Value, "action", group.action, "error", err)
|
||||
for _, item := range group.items {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check each item in the group using the shared permissions
|
||||
for i, item := range group.items {
|
||||
checkReq := group.checkReqs[i]
|
||||
|
||||
allowed, err := s.checkPermission(groupCtx, permissions, checkReq)
|
||||
if err != nil {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false, Error: err.Error()}
|
||||
continue
|
||||
}
|
||||
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: allowed}
|
||||
}
|
||||
}
|
||||
|
||||
span.SetAttributes(attribute.Int("groups_processed", len(groups)))
|
||||
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *authzv1.ListRequest) (*authzv1.ListResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "authz_direct_db.service.List")
|
||||
defer span.End()
|
||||
|
||||
@@ -1829,613 +1829,6 @@ func TestService_CacheList(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_BatchCheck(t *testing.T) {
|
||||
callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||
Claims: jwt.Claims{
|
||||
Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"),
|
||||
Audience: []string{"authzservice"},
|
||||
},
|
||||
Rest: authn.AccessTokenClaims{Namespace: "org-12"},
|
||||
})
|
||||
|
||||
t.Run("Require auth info", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := context.Background()
|
||||
_, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "could not get auth info")
|
||||
})
|
||||
|
||||
t.Run("Empty checks returns empty results", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Empty(t, resp.Results)
|
||||
})
|
||||
|
||||
type batchCheckTestCase struct {
|
||||
name string
|
||||
checks []*authzv1.BatchCheckItem
|
||||
permissions []accesscontrol.Permission
|
||||
folders []store.Folder
|
||||
expectedResults map[string]bool
|
||||
expectedErrors map[string]bool // true if error expected for this correlation ID
|
||||
expectGlobalError bool
|
||||
}
|
||||
|
||||
t.Run("Request validation", func(t *testing.T) {
|
||||
testCases := []batchCheckTestCase{
|
||||
{
|
||||
name: "should return error for invalid namespace",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should return error for namespace mismatch",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-13",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should return error for unknown group",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "unknown.grafana.app",
|
||||
Resource: "unknown",
|
||||
Verb: "get",
|
||||
Name: "u1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should return error for unknown verb",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "unknown",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
expectedErrors: map[string]bool{"check1": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
store := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: tc.permissions,
|
||||
}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: tc.checks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
for corrID, expectedAllowed := range tc.expectedResults {
|
||||
result, ok := resp.Results[corrID]
|
||||
require.True(t, ok, "result for %s not found", corrID)
|
||||
require.Equal(t, expectedAllowed, result.Allowed, "unexpected allowed for %s", corrID)
|
||||
if tc.expectedErrors[corrID] {
|
||||
require.NotEmpty(t, result.Error, "expected error for %s", corrID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("User permission checks", func(t *testing.T) {
|
||||
testCases := []batchCheckTestCase{
|
||||
{
|
||||
name: "should allow user with permission on single resource",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash1"}},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should deny user without permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
},
|
||||
{
|
||||
name: "should handle multiple checks with mixed results",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash3",
|
||||
CorrelationId: "check3",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash3"},
|
||||
},
|
||||
expectedResults: map[string]bool{
|
||||
"check1": true,
|
||||
"check2": false,
|
||||
"check3": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should handle wildcard permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "*", Kind: "*"}},
|
||||
expectedResults: map[string]bool{"check1": true, "check2": true},
|
||||
},
|
||||
{
|
||||
name: "should handle folder inheritance",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
Folder: "child",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "folders:uid:parent", Kind: "folders", Attribute: "uid", Identifier: "parent"},
|
||||
},
|
||||
folders: []store.Folder{
|
||||
{UID: "parent"},
|
||||
{UID: "child", ParentUID: strPtr("parent")},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should handle action sets",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:admin", Scope: "dashboards:uid:dash1"}},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should handle checks across different resources",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "folder.grafana.app",
|
||||
Resource: "folders",
|
||||
Verb: "get",
|
||||
Name: "fold1",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
||||
{Action: "folders:read", Scope: "folders:uid:fold1"},
|
||||
},
|
||||
expectedResults: map[string]bool{"check1": true, "check2": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
store := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: tc.permissions,
|
||||
folders: tc.folders,
|
||||
}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.folderStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
if tc.folders != nil {
|
||||
s.folderCache.Set(ctx, folderCacheKey("org-12"), newFolderTree(tc.folders))
|
||||
}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: tc.checks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, resp.Results, len(tc.expectedResults))
|
||||
for corrID, expectedAllowed := range tc.expectedResults {
|
||||
result, ok := resp.Results[corrID]
|
||||
require.True(t, ok, "result for %s not found", corrID)
|
||||
require.Equal(t, expectedAllowed, result.Allowed, "unexpected allowed for %s", corrID)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Anonymous permission checks", func(t *testing.T) {
|
||||
testCases := []batchCheckTestCase{
|
||||
{
|
||||
name: "should allow anonymous with permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash1"}},
|
||||
expectedResults: map[string]bool{"check1": true},
|
||||
},
|
||||
{
|
||||
name: "should deny anonymous without permission",
|
||||
checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
permissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}},
|
||||
expectedResults: map[string]bool{"check1": false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
store := &fakeStore{userPermissions: tc.permissions}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "anonymous:0",
|
||||
Checks: tc.checks,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
for corrID, expectedAllowed := range tc.expectedResults {
|
||||
result, ok := resp.Results[corrID]
|
||||
require.True(t, ok, "result for %s not found", corrID)
|
||||
require.Equal(t, expectedAllowed, result.Allowed, "unexpected allowed for %s", corrID)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Rendering permission checks", func(t *testing.T) {
|
||||
t.Run("should allow rendering with permission", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "render:0",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("should deny rendering access to another app resources", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "render:0",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "another.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.False(t, resp.Results["check1"].Allowed)
|
||||
require.NotEmpty(t, resp.Results["check1"].Error)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Invalid subject returns errors for all checks", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
store := &fakeStore{}
|
||||
s.store = store
|
||||
s.permissionStore = store
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "invalid:12",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Len(t, resp.Results, 2)
|
||||
for _, result := range resp.Results {
|
||||
require.False(t, result.Allowed)
|
||||
require.NotEmpty(t, result.Error)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Grouping optimization", func(t *testing.T) {
|
||||
t.Run("should batch permission lookups for same action", func(t *testing.T) {
|
||||
s := setupService()
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
fStore := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: []accesscontrol.Permission{
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash1"},
|
||||
{Action: "dashboards:read", Scope: "dashboards:uid:dash2"},
|
||||
},
|
||||
}
|
||||
s.store = fStore
|
||||
s.permissionStore = fStore
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check2",
|
||||
},
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash3",
|
||||
CorrelationId: "check3",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
require.True(t, resp.Results["check2"].Allowed)
|
||||
require.False(t, resp.Results["check3"].Allowed)
|
||||
|
||||
// Verify permissions were fetched only once (1 call for userID + 1 call for basicRole + 1 call for permissions)
|
||||
require.Equal(t, 3, fStore.calls)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_CacheBatchCheck(t *testing.T) {
|
||||
callingService := authn.NewAccessTokenAuthInfo(authn.Claims[authn.AccessTokenClaims]{
|
||||
Claims: jwt.Claims{
|
||||
Subject: types.NewTypeID(types.TypeAccessPolicy, "some-service"),
|
||||
Audience: []string{"authzservice"},
|
||||
},
|
||||
Rest: authn.AccessTokenClaims{Namespace: "org-12"},
|
||||
})
|
||||
|
||||
ctx := types.WithAuthInfo(context.Background(), callingService)
|
||||
userID := &store.UserIdentifiers{UID: "test-uid", ID: 1}
|
||||
|
||||
t.Run("Allow based on cached permissions", func(t *testing.T) {
|
||||
s := setupService()
|
||||
|
||||
s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)
|
||||
s.permCache.Set(ctx, userPermCacheKey("org-12", "test-uid", "dashboards:read"), map[string]bool{"dashboards:uid:dash1": true})
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash1",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("Fallback to database on cache miss", func(t *testing.T) {
|
||||
s := setupService()
|
||||
|
||||
// Populate database but not cache
|
||||
fStore := &fakeStore{
|
||||
userID: userID,
|
||||
userPermissions: []accesscontrol.Permission{{Action: "dashboards:read", Scope: "dashboards:uid:dash2"}},
|
||||
}
|
||||
s.store = fStore
|
||||
s.permissionStore = fStore
|
||||
s.identityStore = &fakeIdentityStore{}
|
||||
|
||||
s.idCache.Set(ctx, userIdentifierCacheKey("org-12", "test-uid"), *userID)
|
||||
|
||||
resp, err := s.BatchCheck(ctx, &authzv1.BatchCheckRequest{
|
||||
Subject: "user:test-uid",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
{
|
||||
Namespace: "org-12",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: "get",
|
||||
Name: "dash2",
|
||||
CorrelationId: "check1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, resp.Results["check1"].Allowed)
|
||||
})
|
||||
}
|
||||
|
||||
func setupService() *Service {
|
||||
cache := cache.NewLocalCache(cache.Config{Expiry: 5 * time.Minute, CleanupInterval: 5 * time.Minute})
|
||||
logger := log.New("authz-rbac-service")
|
||||
|
||||
@@ -13,6 +13,7 @@ type Client interface {
|
||||
authlib.AccessClient
|
||||
Read(ctx context.Context, req *authzextv1.ReadRequest) (*authzextv1.ReadResponse, error)
|
||||
Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error)
|
||||
|
||||
Mutate(ctx context.Context, req *authzextv1.MutateRequest) error
|
||||
Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error)
|
||||
|
||||
@@ -68,11 +68,11 @@ func (c *Client) Write(ctx context.Context, req *authzextv1.WriteRequest) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authlib.zanzana.client.BatchCheck")
|
||||
func (c *Client) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "authlib.zanzana.client.Check")
|
||||
defer span.End()
|
||||
|
||||
return c.authzlibclient.BatchCheck(ctx, id, req)
|
||||
return c.authzext.BatchCheck(ctx, req)
|
||||
}
|
||||
|
||||
func (c *Client) WriteNew(ctx context.Context, req *authzextv1.WriteRequest) error {
|
||||
|
||||
@@ -34,11 +34,8 @@ func (nc NoopClient) Write(ctx context.Context, req *authzextv1.WriteRequest) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
return authlib.BatchCheckResponse{
|
||||
Results: make(map[string]authlib.BatchCheckResult),
|
||||
Zookie: authlib.NoopZookie{},
|
||||
}, nil
|
||||
func (nc NoopClient) BatchCheck(ctx context.Context, req *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nc NoopClient) Mutate(ctx context.Context, req *authzextv1.MutateRequest) error {
|
||||
|
||||
@@ -132,54 +132,3 @@ func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req aut
|
||||
|
||||
return shadowItemChecker, authlib.NoopZookie{}, err
|
||||
}
|
||||
|
||||
func (c *ShadowClient) BatchCheck(ctx context.Context, id authlib.AuthInfo, req authlib.BatchCheckRequest) (authlib.BatchCheckResponse, error) {
|
||||
acResChan := make(chan authlib.BatchCheckResponse, 1)
|
||||
acErrChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
if c.zanzanaClient == nil {
|
||||
return
|
||||
}
|
||||
|
||||
zanzanaCtx := context.WithoutCancel(ctx)
|
||||
zanzanaCtxTimeout, cancel := context.WithTimeout(zanzanaCtx, zanzanaTimeout)
|
||||
defer cancel()
|
||||
|
||||
timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("zanzana"))
|
||||
res, err := c.zanzanaClient.BatchCheck(zanzanaCtxTimeout, id, req)
|
||||
if err != nil {
|
||||
c.logger.Error("Failed to run zanzana batch check", "error", err)
|
||||
}
|
||||
timer.ObserveDuration()
|
||||
|
||||
acRes := <-acResChan
|
||||
acErr := <-acErrChan
|
||||
|
||||
if acErr == nil {
|
||||
// Compare results for each correlation ID
|
||||
for corrID, acResult := range acRes.Results {
|
||||
zanzanaResult, exists := res.Results[corrID]
|
||||
if !exists {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
|
||||
c.logger.Warn("Zanzana batch check missing result", "correlationId", corrID, "user", id.GetUID())
|
||||
continue
|
||||
}
|
||||
if zanzanaResult.Allowed != acResult.Allowed {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
|
||||
c.logger.Warn("Zanzana batch check result does not match", "expected", acResult.Allowed, "actual", zanzanaResult.Allowed, "correlationId", corrID, "user", id.GetUID())
|
||||
} else {
|
||||
c.metrics.evaluationStatusTotal.WithLabelValues("success").Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("rbac"))
|
||||
res, err := c.accessClient.BatchCheck(ctx, id, req)
|
||||
timer.ObserveDuration()
|
||||
acResChan <- res
|
||||
acErrChan <- err
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
iamv0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
)
|
||||
|
||||
type typeInfo struct {
|
||||
@@ -72,7 +73,7 @@ func NewResourceInfoFromCheck(r *authzv1.CheckRequest) ResourceInfo {
|
||||
return resource
|
||||
}
|
||||
|
||||
func NewResourceInfoFromBatchItem(i *authzv1.BatchCheckItem) ResourceInfo {
|
||||
func NewResourceInfoFromBatchItem(i *authzextv1.BatchCheckItem) ResourceInfo {
|
||||
typ, relations := getTypeAndRelations(i.GetGroup(), i.GetResource())
|
||||
return newResource(
|
||||
typ,
|
||||
|
||||
@@ -2,463 +2,97 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
// checkKey represents a unique check to be performed
|
||||
type checkKey struct {
|
||||
relation string
|
||||
object string
|
||||
}
|
||||
|
||||
// batchCheckBuilder encapsulates state for building OpenFGA batch checks
|
||||
type batchCheckBuilder struct {
|
||||
subject string
|
||||
contextuals *openfgav1.ContextualTupleKeys
|
||||
checks []*openfgav1.BatchCheckItem
|
||||
checksSeen map[checkKey]bool
|
||||
checkMapping map[string]checkKey
|
||||
counter int
|
||||
}
|
||||
|
||||
func newBatchCheckBuilder(subject string, contextuals *openfgav1.ContextualTupleKeys) *batchCheckBuilder {
|
||||
return &batchCheckBuilder{
|
||||
subject: subject,
|
||||
contextuals: contextuals,
|
||||
checks: make([]*openfgav1.BatchCheckItem, 0),
|
||||
checksSeen: make(map[checkKey]bool),
|
||||
checkMapping: make(map[string]checkKey),
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *batchCheckBuilder) addCheck(relation, object string, context *structpb.Struct) {
|
||||
if object == "" {
|
||||
return
|
||||
}
|
||||
|
||||
key := checkKey{relation: relation, object: object}
|
||||
if b.checksSeen[key] {
|
||||
return
|
||||
}
|
||||
b.checksSeen[key] = true
|
||||
|
||||
correlationID := fmt.Sprintf("c%d", b.counter)
|
||||
b.counter++
|
||||
|
||||
b.checks = append(b.checks, &openfgav1.BatchCheckItem{
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: b.subject,
|
||||
Relation: relation,
|
||||
Object: object,
|
||||
},
|
||||
ContextualTuples: b.contextuals,
|
||||
Context: context,
|
||||
CorrelationId: correlationID,
|
||||
})
|
||||
b.checkMapping[correlationID] = key
|
||||
}
|
||||
|
||||
// BatchCheck implements authzv1.AuthzServiceServer.BatchCheck
|
||||
// This performs multiple access checks in a single request using OpenFGA's native BatchCheck API.
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest) (*authzextv1.BatchCheckResponse, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "server.BatchCheck")
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(attribute.Int("check_count", len(r.GetChecks())))
|
||||
if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func(t time.Time) {
|
||||
s.metrics.requestDurationSeconds.WithLabelValues("server.BatchCheck", "").Observe(time.Since(t).Seconds())
|
||||
}(time.Now())
|
||||
batchRes := &authzextv1.BatchCheckResponse{
|
||||
Groups: make(map[string]*authzextv1.BatchCheckGroupResource),
|
||||
}
|
||||
|
||||
res, err := s.batchCheck(ctx, r)
|
||||
store, err := s.getStoreInfo(ctx, r.GetNamespace())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform batch check request", "error", err)
|
||||
return nil, fmt.Errorf("failed to perform batch check request: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) batchCheck(ctx context.Context, r *authzv1.BatchCheckRequest) (*authzv1.BatchCheckResponse, error) {
|
||||
items := r.GetChecks()
|
||||
if len(items) == 0 {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: make(map[string]*authzv1.BatchCheckResult),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Group items by namespace
|
||||
itemsByNamespace := make(map[string][]*authzv1.BatchCheckItem)
|
||||
for _, item := range items {
|
||||
ns := item.GetNamespace()
|
||||
itemsByNamespace[ns] = append(itemsByNamespace[ns], item)
|
||||
}
|
||||
|
||||
// Authorize and get store info for each namespace
|
||||
stores := make(map[string]*storeInfo)
|
||||
for namespace := range itemsByNamespace {
|
||||
if err := authorize(ctx, namespace, s.cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store, err := s.getStoreInfo(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stores[namespace] = store
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contextuals, err := s.getContextuals(r.GetSubject())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make(map[string]*authzv1.BatchCheckResult, len(items))
|
||||
subject := r.GetSubject()
|
||||
groupResourceAccess := make(map[string]bool)
|
||||
|
||||
// Process each namespace separately
|
||||
for namespace, nsItems := range itemsByNamespace {
|
||||
store := stores[namespace]
|
||||
|
||||
// Phase 1: Check GroupResource access (broadest permissions)
|
||||
// Example: user has "get" on "dashboards" group_resource → all dashboards allowed
|
||||
s.runGroupResourcePhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
|
||||
// Phase 2: Check folder permission inheritance (can_get, can_create, etc. on parent folder)
|
||||
// Example: user has "can_get" on folder-A → all dashboards in folder-A allowed
|
||||
s.runFolderPermissionPhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
|
||||
// Phase 3: Check folder subresource access (folder_get, folder_create, etc.)
|
||||
// Example: user has "folder_get" on folder-A → dashboards in folder-A allowed via subresource
|
||||
s.runFolderSubresourcePhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
|
||||
// Phase 4: Check direct resource access
|
||||
// Example: user has "get" directly on dashboard-123
|
||||
s.runDirectResourcePhase(ctx, store, subject, nsItems, contextuals, results)
|
||||
}
|
||||
|
||||
// Mark any remaining unresolved items as denied
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; !resolved {
|
||||
results[item.GetCorrelationId()] = &authzv1.BatchCheckResult{Allowed: false}
|
||||
}
|
||||
}
|
||||
|
||||
return s.buildResponse(results), nil
|
||||
}
|
||||
|
||||
func (s *Server) buildResponse(results map[string]*authzv1.BatchCheckResult) *authzv1.BatchCheckResponse {
|
||||
return &authzv1.BatchCheckResponse{
|
||||
Results: results,
|
||||
Zookie: &authzv1.Zookie{Timestamp: time.Now().UnixMilli()},
|
||||
}
|
||||
}
|
||||
|
||||
// runGroupResourcePhase checks if the user has GroupResource-level access.
|
||||
// This is the broadest permission - if allowed, all items in that group are allowed.
|
||||
func (s *Server) runGroupResourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
// Group items by their GroupResource
|
||||
type grInfo struct {
|
||||
relation string
|
||||
grIdent string
|
||||
items []string // correlation IDs
|
||||
}
|
||||
groupedItems := make(map[string]*grInfo) // groupResource -> info
|
||||
|
||||
for _, item := range items {
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
if !common.IsGroupResourceRelation(relation) {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
gr := resource.GroupResource()
|
||||
|
||||
if _, exists := groupedItems[gr]; !exists {
|
||||
groupedItems[gr] = &grInfo{
|
||||
relation: relation,
|
||||
grIdent: resource.GroupResourceIdent(),
|
||||
items: make([]string, 0),
|
||||
}
|
||||
}
|
||||
groupedItems[gr].items = append(groupedItems[gr].items, item.GetCorrelationId())
|
||||
}
|
||||
|
||||
if len(groupedItems) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build batch check for unique GroupResources
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
grCheckMapping := make(map[string]string) // OpenFGA correlationID -> groupResource
|
||||
|
||||
for gr, info := range groupedItems {
|
||||
correlationID := fmt.Sprintf("gr%d", builder.counter)
|
||||
builder.counter++
|
||||
builder.checks = append(builder.checks, &openfgav1.BatchCheckItem{
|
||||
TupleKey: &openfgav1.CheckRequestTupleKey{
|
||||
User: subject,
|
||||
Relation: info.relation,
|
||||
Object: info.grIdent,
|
||||
},
|
||||
ContextualTuples: contextuals,
|
||||
CorrelationId: correlationID,
|
||||
})
|
||||
grCheckMapping[correlationID] = gr
|
||||
}
|
||||
|
||||
openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Checks: builder.checks,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to check group resource access", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Mark all items in allowed GroupResources
|
||||
for correlationID, result := range openfgaRes.GetResult() {
|
||||
gr := grCheckMapping[correlationID]
|
||||
if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok && allowed.Allowed {
|
||||
for _, itemCorrelationID := range groupedItems[gr].items {
|
||||
results[itemCorrelationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runFolderPermissionPhase checks folder permission inheritance (can_get, can_create, etc.).
|
||||
// This applies to folder-based resources like dashboards, panels, etc.
|
||||
func (s *Server) runFolderPermissionPhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string) // checkKey -> correlation IDs
|
||||
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
folderIdent := resource.FolderIdent()
|
||||
|
||||
// Only folder-based generic resources use folder permission inheritance
|
||||
if !resource.IsGeneric() || folderIdent == "" || !isFolderPermissionBasedResource(resource.GroupResource()) {
|
||||
continue
|
||||
}
|
||||
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
rel := common.FolderPermissionRelation(relation)
|
||||
key := checkKey{relation: rel, object: folderIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(rel, folderIdent, resource.Context())
|
||||
}
|
||||
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed folder permission phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Mark items allowed by folder permissions
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runFolderSubresourcePhase checks folder subresource access (folder_get, folder_create, etc.).
|
||||
func (s *Server) runFolderSubresourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string)
|
||||
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
|
||||
var objectIdent string
|
||||
var subresRel string
|
||||
|
||||
if resource.IsGeneric() {
|
||||
// Generic resources: check subresource on folder
|
||||
folderIdent := resource.FolderIdent()
|
||||
if folderIdent == "" {
|
||||
continue
|
||||
}
|
||||
subresRel = common.SubresourceRelation(relation)
|
||||
if !common.IsSubresourceRelation(subresRel) {
|
||||
continue
|
||||
}
|
||||
objectIdent = folderIdent
|
||||
} else {
|
||||
// Typed resources: check subresource on the resource itself
|
||||
if !resource.HasSubresource() || !resource.IsValidRelation(relation) {
|
||||
continue
|
||||
}
|
||||
objectIdent = resource.ResourceIdent()
|
||||
if objectIdent == "" {
|
||||
continue
|
||||
}
|
||||
subresRel = common.SubresourceRelation(relation)
|
||||
}
|
||||
|
||||
key := checkKey{relation: subresRel, object: objectIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(subresRel, objectIdent, resource.Context())
|
||||
}
|
||||
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed folder subresource phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runDirectResourcePhase checks direct resource access.
|
||||
func (s *Server) runDirectResourcePhase(
|
||||
ctx context.Context,
|
||||
store *storeInfo,
|
||||
subject string,
|
||||
items []*authzv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
results map[string]*authzv1.BatchCheckResult,
|
||||
) {
|
||||
builder := newBatchCheckBuilder(subject, contextuals)
|
||||
checkToItems := make(map[checkKey][]string)
|
||||
|
||||
for _, item := range items {
|
||||
if _, resolved := results[item.GetCorrelationId()]; resolved {
|
||||
continue
|
||||
}
|
||||
|
||||
resource := common.NewResourceInfoFromBatchItem(item)
|
||||
relation := common.VerbMapping[item.GetVerb()]
|
||||
|
||||
if !resource.IsValidRelation(relation) {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceIdent := resource.ResourceIdent()
|
||||
if resourceIdent == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// For folders, use the computed permission relation
|
||||
checkRelation := relation
|
||||
if resource.Type() == common.TypeFolder {
|
||||
checkRelation = common.FolderPermissionRelation(relation)
|
||||
}
|
||||
|
||||
key := checkKey{relation: checkRelation, object: resourceIdent}
|
||||
checkToItems[key] = append(checkToItems[key], item.GetCorrelationId())
|
||||
builder.addCheck(checkRelation, resourceIdent, resource.Context())
|
||||
}
|
||||
|
||||
if len(builder.checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
checkResults, err := s.executeOpenFGABatchChecks(ctx, store, builder)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed direct resource phase", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for key, allowed := range checkResults {
|
||||
if allowed {
|
||||
for _, correlationID := range checkToItems[key] {
|
||||
results[correlationID] = &authzv1.BatchCheckResult{Allowed: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeOpenFGABatchChecks executes the OpenFGA batch checks in chunks and returns results
|
||||
func (s *Server) executeOpenFGABatchChecks(ctx context.Context, store *storeInfo, builder *batchCheckBuilder) (map[checkKey]bool, error) {
|
||||
const maxChecksPerBatch = 50
|
||||
checkResults := make(map[checkKey]bool)
|
||||
|
||||
for i := 0; i < len(builder.checks); i += maxChecksPerBatch {
|
||||
end := i + maxChecksPerBatch
|
||||
if end > len(builder.checks) {
|
||||
end = len(builder.checks)
|
||||
}
|
||||
|
||||
openfgaRes, err := s.openfgaClient.BatchCheck(ctx, &openfgav1.BatchCheckRequest{
|
||||
StoreId: store.ID,
|
||||
AuthorizationModelId: store.ModelID,
|
||||
Checks: builder.checks[i:end],
|
||||
})
|
||||
for _, item := range r.GetItems() {
|
||||
res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to perform OpenFGA batch check: %w", err)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process results
|
||||
for correlationID, result := range openfgaRes.GetResult() {
|
||||
key, ok := builder.checkMapping[correlationID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if allowed, ok := result.GetCheckResult().(*openfgav1.BatchCheckSingleResult_Allowed); ok {
|
||||
checkResults[key] = allowed.Allowed
|
||||
groupResource := common.FormatGroupResource(item.GetGroup(), item.GetResource(), item.GetSubresource())
|
||||
if _, ok := batchRes.Groups[groupResource]; !ok {
|
||||
batchRes.Groups[groupResource] = &authzextv1.BatchCheckGroupResource{
|
||||
Items: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
batchRes.Groups[groupResource].Items[item.GetName()] = res.GetAllowed()
|
||||
}
|
||||
|
||||
return checkResults, nil
|
||||
return batchRes, nil
|
||||
}
|
||||
|
||||
func (s *Server) batchCheckItem(
|
||||
ctx context.Context,
|
||||
r *authzextv1.BatchCheckRequest,
|
||||
item *authzextv1.BatchCheckItem,
|
||||
contextuals *openfgav1.ContextualTupleKeys,
|
||||
store *storeInfo,
|
||||
groupResourceAccess map[string]bool,
|
||||
) (*authzv1.CheckResponse, error) {
|
||||
var (
|
||||
relation = common.VerbMapping[item.GetVerb()]
|
||||
resource = common.NewResourceInfoFromBatchItem(item)
|
||||
groupResource = resource.GroupResource()
|
||||
)
|
||||
|
||||
allowed, ok := groupResourceAccess[groupResource]
|
||||
if !ok {
|
||||
res, err := s.checkGroupResource(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowed = res.GetAllowed()
|
||||
groupResourceAccess[groupResource] = res.GetAllowed()
|
||||
}
|
||||
|
||||
if allowed {
|
||||
return &authzv1.CheckResponse{Allowed: true}, nil
|
||||
}
|
||||
|
||||
if resource.IsGeneric() {
|
||||
return s.checkGeneric(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
}
|
||||
|
||||
return s.checkTyped(ctx, r.GetSubject(), relation, resource, contextuals, store)
|
||||
}
|
||||
|
||||
@@ -1,302 +1,193 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
|
||||
func testBatchCheck(t *testing.T, server *Server) {
|
||||
// Helper to create a batch check request
|
||||
newReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest {
|
||||
return &authzv1.BatchCheckRequest{
|
||||
Subject: subject,
|
||||
Checks: items,
|
||||
newReq := func(subject, verb, group, resource, subresource string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
for i, item := range items {
|
||||
items[i] = &authzextv1.BatchCheckItem{
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: item.GetName(),
|
||||
Folder: item.GetFolder(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create a batch check item with correlation ID (uses default namespace)
|
||||
newItem := func(verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s", group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: namespace,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
return &authzextv1.BatchCheckRequest{
|
||||
Namespace: namespace,
|
||||
Subject: subject,
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("user:1 should only be able to read resource:dashboard.grafana.app/dashboards/1", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:1", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:2 should be able to read resource:dashboard.grafana.app/dashboards/{1,2} through group_resource", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:2", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
// user:2 has group_resource access, so both should be allowed
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
assert.Len(t, res.Groups[groupResource].Items, 2)
|
||||
})
|
||||
|
||||
t.Run("user:3 should be able to read resource:dashboard.grafana.app/dashboards/1 with set relation", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:3", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:4 should be able to read all dashboard.grafana.app/dashboards in folder 1 and 3", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "3", "2"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "3"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:4", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "3"},
|
||||
{Name: "3", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 3)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "3", "2")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "3")].Allowed)
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.True(t, res.Groups[groupResource].Items["2"])
|
||||
assert.False(t, res.Groups[groupResource].Items["3"])
|
||||
})
|
||||
|
||||
t.Run("user:5 should be able to read resource:dashboard.grafana.app/dashboards/1 through folder with set relation", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:5", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1", Folder: "1"},
|
||||
{Name: "2", Folder: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:6 should be able to read folder 1", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"),
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"),
|
||||
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:6", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed)
|
||||
assert.True(t, res.Groups[groupResource].Items["1"])
|
||||
assert.False(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:7 should be able to read folder {1,2} through group_resource access", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "1"),
|
||||
newItem(utils.VerbGet, folderGroup, folderResource, "", "", "2"),
|
||||
groupResource := common.FormatGroupResource(folderGroup, folderResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:7", utils.VerbGet, folderGroup, folderResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "1"},
|
||||
{Name: "2"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", folderGroup, folderResource, "", "2")].Allowed)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["1"])
|
||||
require.True(t, res.Groups[groupResource].Items["2"])
|
||||
})
|
||||
|
||||
t.Run("user:8 should be able to read all resource:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "6", "20"),
|
||||
t.Run("user:8 should be able to read all resoruce:dashboard.grafana.app/dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:8", utils.VerbGet, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
})
|
||||
|
||||
t.Run("user:9 should be able to create dashboards in folder 6 through folder 5", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "10"),
|
||||
newItem(utils.VerbCreate, dashboardGroup, dashboardResource, "", "6", "20"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, "")
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:9", utils.VerbCreate, dashboardGroup, dashboardResource, "", []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "20", Folder: "6"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "20")].Allowed)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 2)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["20"])
|
||||
})
|
||||
|
||||
t.Run("user:10 should be able to get dashboard status for 10 and 11", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:10", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.False(t, res.Groups[groupResource].Items["12"])
|
||||
})
|
||||
|
||||
t.Run("user:11 should be able to get dashboard status for 10, 11 and 12 through group_resource", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:11", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "6"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
t.Log(res.Groups)
|
||||
require.Len(t, res.Groups[groupResource].Items, 3)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.True(t, res.Groups[groupResource].Items["12"])
|
||||
})
|
||||
|
||||
t.Run("user:12 should be able to get dashboard status in folder 5 and 6", func(t *testing.T) {
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", []*authzv1.BatchCheckItem{
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "5", "10"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "11"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "6", "12"),
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, "1", "13"),
|
||||
groupResource := common.FormatGroupResource(dashboardGroup, dashboardResource, statusSubresource)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), newReq("user:12", utils.VerbGet, dashboardGroup, dashboardResource, statusSubresource, []*authzextv1.BatchCheckItem{
|
||||
{Name: "10", Folder: "5"},
|
||||
{Name: "11", Folder: "6"},
|
||||
{Name: "12", Folder: "6"},
|
||||
{Name: "13", Folder: "1"},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 4)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "5", "10")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "11")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "6", "12")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "13")].Allowed)
|
||||
})
|
||||
|
||||
// Cross-namespace tests
|
||||
t.Run("cross-namespace: items with explicit namespace should be authorized against their own namespace", func(t *testing.T) {
|
||||
// Helper to create item with explicit namespace
|
||||
newItemWithNamespace := func(ns, verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s-%s", ns, group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
// user:1 has access to dashboard 1 in folder 1 in "default" namespace
|
||||
// Both items use explicit namespace
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), &authzv1.BatchCheckRequest{
|
||||
Subject: "user:1",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
// Item in default namespace (should be allowed - user:1 has access)
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
// Another item in default namespace with different correlation ID
|
||||
newItem(utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 2)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("cross-namespace: items from different namespaces in same batch", func(t *testing.T) {
|
||||
newItemWithNamespace := func(ns, verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s-%s", ns, group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
// user:2 has group_resource access in "default" namespace
|
||||
// They should have access in default but not in other-namespace (no tuples there)
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), &authzv1.BatchCheckRequest{
|
||||
Subject: "user:2",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
// Items in default namespace (should be allowed - user:2 has group_resource access)
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
// Items in other-namespace (should be denied - no tuples in other-namespace)
|
||||
newItemWithNamespace("other-namespace", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
newItemWithNamespace("other-namespace", utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 4)
|
||||
|
||||
// Default namespace items should be allowed
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
// Other namespace items should be denied (no permissions in that namespace)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", "other-namespace", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", "other-namespace", dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
})
|
||||
|
||||
t.Run("cross-namespace: mixed results across multiple namespaces", func(t *testing.T) {
|
||||
newItemWithNamespace := func(ns, verb, group, resource, subresource, folder, name string) *authzv1.BatchCheckItem {
|
||||
correlationID := fmt.Sprintf("%s-%s-%s-%s-%s", ns, group, resource, folder, name)
|
||||
return &authzv1.BatchCheckItem{
|
||||
Namespace: ns,
|
||||
Verb: verb,
|
||||
Group: group,
|
||||
Resource: resource,
|
||||
Subresource: subresource,
|
||||
Name: name,
|
||||
Folder: folder,
|
||||
CorrelationId: correlationID,
|
||||
}
|
||||
}
|
||||
|
||||
// user:1 has specific access to dashboard 1 in folder 1
|
||||
// user:2 would have broader access, but we're testing user:1
|
||||
res, err := server.BatchCheck(newContextWithNamespace(), &authzv1.BatchCheckRequest{
|
||||
Subject: "user:1",
|
||||
Checks: []*authzv1.BatchCheckItem{
|
||||
// Allowed in default namespace
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
// Denied in default namespace (user:1 doesn't have access to dashboard 2)
|
||||
newItemWithNamespace(namespace, utils.VerbGet, dashboardGroup, dashboardResource, "", "2", "2"),
|
||||
// Denied in other-namespace (no tuples)
|
||||
newItemWithNamespace("other-namespace", utils.VerbGet, dashboardGroup, dashboardResource, "", "1", "1"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Results, 3)
|
||||
|
||||
assert.True(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", namespace, dashboardGroup, dashboardResource, "2", "2")].Allowed)
|
||||
assert.False(t, res.Results[fmt.Sprintf("%s-%s-%s-%s-%s", "other-namespace", dashboardGroup, dashboardResource, "1", "1")].Allowed)
|
||||
require.Len(t, res.Groups[groupResource].Items, 4)
|
||||
require.True(t, res.Groups[groupResource].Items["10"])
|
||||
require.True(t, res.Groups[groupResource].Items["11"])
|
||||
require.True(t, res.Groups[groupResource].Items["12"])
|
||||
require.False(t, res.Groups[groupResource].Items["13"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/store"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
@@ -36,14 +37,14 @@ const (
|
||||
// Timeout for List operations
|
||||
listTimeout = 30 * time.Second
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch.
|
||||
batchCheckSize = 50
|
||||
|
||||
// Resource type constants for benchmarks
|
||||
benchDashboardGroup = "dashboard.grafana.app"
|
||||
benchDashboardResource = "dashboards"
|
||||
benchFolderGroup = "folder.grafana.app"
|
||||
benchFolderResource = "folders"
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests with 50 items per batch.
|
||||
batchCheckSize = 50
|
||||
)
|
||||
|
||||
// benchmarkData holds all the generated test data for benchmarks
|
||||
@@ -337,14 +338,6 @@ func setupBenchmarkServer(b *testing.B) (*Server, *benchmarkData) {
|
||||
}
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
cfg.ZanzanaServer.CacheSettings.CheckCacheLimit = 100000 // Cache check results
|
||||
cfg.ZanzanaServer.CacheSettings.CheckQueryCacheEnabled = true // Cache check subproblems
|
||||
cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheEnabled = true // Cache DB iterators for checks
|
||||
cfg.ZanzanaServer.CacheSettings.CheckIteratorCacheMaxResults = 10000 // Max results per iterator
|
||||
cfg.ZanzanaServer.CacheSettings.SharedIteratorEnabled = true // Share iterators across concurrent checks
|
||||
cfg.ZanzanaServer.CacheSettings.SharedIteratorLimit = 10000 // Max shared iterators
|
||||
|
||||
testStore := sqlstore.NewTestStore(b, sqlstore.WithCfg(cfg))
|
||||
|
||||
openFGAStore, err := store.NewEmbeddedStore(cfg, testStore, log.NewNopLogger())
|
||||
@@ -580,64 +573,58 @@ func BenchmarkCheck(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBatchCheck measures the performance of BatchCheck requests
|
||||
func BenchmarkBatchCheck(b *testing.B) {
|
||||
srv, data := setupBenchmarkServer(b)
|
||||
ctx := newContextWithNamespace()
|
||||
|
||||
// Helper to create batch check requests using the new authzv1 API
|
||||
newBatchCheckReq := func(subject string, items []*authzv1.BatchCheckItem) *authzv1.BatchCheckRequest {
|
||||
return &authzv1.BatchCheckRequest{
|
||||
Subject: subject,
|
||||
Checks: items,
|
||||
// Helper to create batch check requests
|
||||
newBatchCheckReq := func(subject string, items []*authzextv1.BatchCheckItem) *authzextv1.BatchCheckRequest {
|
||||
return &authzextv1.BatchCheckRequest{
|
||||
Namespace: benchNamespace,
|
||||
Subject: subject,
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create batch items for resources in folders
|
||||
createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzv1.BatchCheckItem {
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
createBatchItems := func(resources []string, resourceFolders map[string]string) []*authzextv1.BatchCheckItem {
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
for i := 0; i < batchCheckSize && i < len(resources); i++ {
|
||||
resource := resources[i]
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: resource,
|
||||
Folder: resourceFolders[resource],
|
||||
CorrelationId: fmt.Sprintf("item-%d", i),
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: resource,
|
||||
Folder: resourceFolders[resource],
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// Helper to create batch items for folders at a specific depth
|
||||
createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzv1.BatchCheckItem {
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
createFolderBatchItems := func(folders []string, depth int, folderDepths map[string]int) []*authzextv1.BatchCheckItem {
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
for _, folder := range folders {
|
||||
if folderDepths[folder] == depth && len(items) < batchCheckSize {
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-in-%s", folder),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", len(items)),
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-in-%s", folder),
|
||||
Folder: folder,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Fill remaining slots if needed
|
||||
for len(items) < batchCheckSize && len(folders) > 0 {
|
||||
folder := folders[len(items)%len(folders)]
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", len(items)),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", len(items)),
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", len(items)),
|
||||
Folder: folder,
|
||||
})
|
||||
}
|
||||
return items
|
||||
@@ -649,7 +636,6 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with group_resource permission - should have access to everything
|
||||
user := data.users[0]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has group_resource permission (all access)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -657,7 +643,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
@@ -665,7 +651,6 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with folder permission on shallow folder
|
||||
user := data.users[usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, 1, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth 1", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -673,7 +658,7 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
@@ -681,7 +666,6 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with folder permission on mid-depth folder
|
||||
user := data.users[2*usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, 4, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth 4", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -689,7 +673,22 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("FolderInheritance/Depth7", func(b *testing.B) {
|
||||
// Check access on deepest folders (worst case for inheritance traversal)
|
||||
user := data.users[usersPerPattern]
|
||||
items := createFolderBatchItems(data.folders, data.maxDepth, data.folderDepths)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
@@ -697,7 +696,6 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with direct resource permission
|
||||
user := data.users[4*usersPerPattern]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has direct resource permission", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -705,7 +703,22 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("TeamMembership", func(b *testing.B) {
|
||||
// User who is a team member, team has folder permission
|
||||
user := data.users[5*usersPerPattern]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
@@ -713,7 +726,6 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
// User with no permissions - tests denial path
|
||||
user := data.users[len(data.users)-1]
|
||||
items := createBatchItems(data.resources, data.resourceFolders)
|
||||
b.Logf("Testing BatchCheck with %d items, user has NO permissions (denial case)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -721,29 +733,24 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MixedAccess", func(b *testing.B) {
|
||||
// Create items from different folders - user has access to some but not all
|
||||
user := data.users[3*usersPerPattern] // folder-scoped resource permission
|
||||
items := make([]*authzv1.BatchCheckItem, 0, batchCheckSize)
|
||||
|
||||
// Mix of accessible and inaccessible resources
|
||||
b.Run("MixedFolders", func(b *testing.B) {
|
||||
// Batch of items across different folder depths
|
||||
user := data.users[usersPerPattern]
|
||||
items := make([]*authzextv1.BatchCheckItem, 0, batchCheckSize)
|
||||
for i := 0; i < batchCheckSize; i++ {
|
||||
folder := data.folders[i%len(data.folders)]
|
||||
items = append(items, &authzv1.BatchCheckItem{
|
||||
Namespace: benchNamespace,
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", i),
|
||||
Folder: folder,
|
||||
CorrelationId: fmt.Sprintf("item-%d", i),
|
||||
items = append(items, &authzextv1.BatchCheckItem{
|
||||
Verb: utils.VerbGet,
|
||||
Group: benchDashboardGroup,
|
||||
Resource: benchDashboardResource,
|
||||
Name: fmt.Sprintf("resource-%d", i),
|
||||
Folder: folder,
|
||||
})
|
||||
}
|
||||
b.Logf("Testing BatchCheck with %d items, user has mixed access (some allowed, some denied)", len(items))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -751,31 +758,9 @@ func BenchmarkBatchCheck(b *testing.B) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
_ = res.Groups
|
||||
}
|
||||
})
|
||||
|
||||
// Test BatchCheck at various folder depths
|
||||
for depth := 0; depth <= data.maxDepth; depth++ {
|
||||
depth := depth // capture for closure
|
||||
if len(data.foldersByDepth[depth]) == 0 {
|
||||
continue
|
||||
}
|
||||
b.Run(fmt.Sprintf("ByDepth/Depth%d", depth), func(b *testing.B) {
|
||||
user := fmt.Sprintf("user:depth-%d-access", depth)
|
||||
items := createFolderBatchItems(data.folders, depth, data.folderDepths)
|
||||
b.Logf("Testing BatchCheck with %d items at depth %d", len(items), depth)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
res, err := srv.BatchCheck(ctx, newBatchCheckReq(user, items))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_ = res.Results
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkList measures the performance of List requests (Compile equivalent)
|
||||
|
||||
@@ -1155,14 +1155,7 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "passwordlessMagicLinkAuthentication",
|
||||
Description: "Enable passwordless login via magic link authentication",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: identityAccessTeam,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
{
|
||||
Name: "exploreMetricsRelatedLogs",
|
||||
Description: "Display Related Logs in Grafana Metrics Drilldown",
|
||||
Stage: FeatureStageExperimental,
|
||||
|
||||
1
pkg/services/featuremgmt/toggles_gen.csv
generated
1
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -160,7 +160,6 @@ timeRangePan,experimental,@grafana/dataviz-squad,false,false,true
|
||||
newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true
|
||||
azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false
|
||||
playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false
|
||||
exploreMetricsRelatedLogs,experimental,@grafana/observability-metrics,false,false,true
|
||||
prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true
|
||||
enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false
|
||||
|
||||
|
4
pkg/services/featuremgmt/toggles_gen.go
generated
4
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -483,10 +483,6 @@ const (
|
||||
// Enables experimental reconciler for playlists
|
||||
FlagPlaylistsReconciler = "playlistsReconciler"
|
||||
|
||||
// FlagPasswordlessMagicLinkAuthentication
|
||||
// Enable passwordless login via magic link authentication
|
||||
FlagPasswordlessMagicLinkAuthentication = "passwordlessMagicLinkAuthentication"
|
||||
|
||||
// FlagEnableExtensionsAdminPage
|
||||
// Enables the extension admin page regardless of development mode
|
||||
FlagEnableExtensionsAdminPage = "enableExtensionsAdminPage"
|
||||
|
||||
3
pkg/services/featuremgmt/toggles_gen.json
generated
3
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -2661,7 +2661,8 @@
|
||||
"metadata": {
|
||||
"name": "passwordlessMagicLinkAuthentication",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-11-14T13:50:55Z"
|
||||
"creationTimestamp": "2024-11-14T13:50:55Z",
|
||||
"deletionTimestamp": "2026-01-08T15:33:29Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable passwordless login via magic link authentication",
|
||||
|
||||
@@ -647,12 +647,6 @@
|
||||
},
|
||||
"BacktestConfig": {
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -662,8 +656,16 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"exec_err_state": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"for": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"format": "date-time",
|
||||
@@ -672,12 +674,22 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"keep_firing_for": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"missing_series_evals_to_resolve": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"namespace_uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"no_data_state": {
|
||||
"enum": [
|
||||
"Alerting",
|
||||
@@ -686,12 +698,18 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"rule_group": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1813,6 +1831,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -1823,6 +1847,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/GettableExtendedRuleNode"
|
||||
@@ -3142,6 +3172,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -3152,6 +3188,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PostableExtendedRuleNode"
|
||||
@@ -3817,6 +3859,14 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RemoteWriteConfig": {
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ResponseDetails": {
|
||||
"properties": {
|
||||
"msg": {
|
||||
@@ -4093,6 +4143,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -4103,6 +4159,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/GettableExtendedRuleNode"
|
||||
|
||||
@@ -284,11 +284,20 @@ type PostableRuleGroupConfig struct {
|
||||
|
||||
// fields below are used by Mimir/Loki rulers
|
||||
|
||||
SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"`
|
||||
EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"`
|
||||
QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"`
|
||||
AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"`
|
||||
Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`
|
||||
SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"`
|
||||
EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"`
|
||||
QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"`
|
||||
AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"`
|
||||
Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`
|
||||
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
|
||||
|
||||
// GEM Ruler.
|
||||
|
||||
RWConfigs []RemoteWriteConfig `yaml:"remote_write,omitempty" json:"remote_write,omitempty"`
|
||||
}
|
||||
|
||||
type RemoteWriteConfig struct {
|
||||
URL string `yaml:"url,omitempty" json:"url,omitempty"`
|
||||
}
|
||||
|
||||
func (c *PostableRuleGroupConfig) UnmarshalJSON(b []byte) error {
|
||||
@@ -328,8 +337,8 @@ func (c *PostableRuleGroupConfig) validate() error {
|
||||
return fmt.Errorf("cannot mix Grafana & Prometheus style rules")
|
||||
}
|
||||
|
||||
if hasGrafRules && (len(c.SourceTenants) > 0 || c.EvaluationDelay != nil || c.QueryOffset != nil || c.AlignEvaluationTimeOnInterval || c.Limit > 0) {
|
||||
return fmt.Errorf("fields source_tenants, evaluation_delay, query_offset, align_evaluation_time_on_interval and limit are not supported for Grafana rules")
|
||||
if hasGrafRules && (len(c.SourceTenants) > 0 || c.EvaluationDelay != nil || c.QueryOffset != nil || c.AlignEvaluationTimeOnInterval || c.Limit > 0 || len(c.Labels) > 0 || len(c.RWConfigs) > 0) {
|
||||
return fmt.Errorf("fields source_tenants, evaluation_delay, query_offset, align_evaluation_time_on_interval, limit, labels, and remote_write are not supported for Grafana rules")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -345,11 +354,16 @@ type GettableRuleGroupConfig struct {
|
||||
|
||||
// fields below are used by Mimir/Loki rulers
|
||||
|
||||
SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"`
|
||||
EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"`
|
||||
QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"`
|
||||
AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"`
|
||||
Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`
|
||||
SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"`
|
||||
EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"`
|
||||
QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"`
|
||||
AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"`
|
||||
Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`
|
||||
Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
|
||||
|
||||
// GEM Ruler.
|
||||
|
||||
RWConfigs []RemoteWriteConfig `yaml:"remote_write,omitempty" json:"remote_write,omitempty"`
|
||||
}
|
||||
|
||||
func (c *GettableRuleGroupConfig) UnmarshalJSON(b []byte) error {
|
||||
|
||||
@@ -647,12 +647,6 @@
|
||||
},
|
||||
"BacktestConfig": {
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -662,8 +656,16 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"exec_err_state": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"for": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"format": "date-time",
|
||||
@@ -672,12 +674,22 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"keep_firing_for": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"missing_series_evals_to_resolve": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"namespace_uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"no_data_state": {
|
||||
"enum": [
|
||||
"Alerting",
|
||||
@@ -686,12 +698,18 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"rule_group": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1813,6 +1831,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -1823,6 +1847,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/GettableExtendedRuleNode"
|
||||
@@ -3142,6 +3172,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -3152,6 +3188,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PostableExtendedRuleNode"
|
||||
@@ -3817,6 +3859,14 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RemoteWriteConfig": {
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ResponseDetails": {
|
||||
"properties": {
|
||||
"msg": {
|
||||
@@ -4093,6 +4143,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -4103,6 +4159,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/GettableExtendedRuleNode"
|
||||
|
||||
@@ -5072,12 +5072,6 @@
|
||||
"BacktestConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -5087,8 +5081,16 @@
|
||||
"$ref": "#/definitions/AlertQuery"
|
||||
}
|
||||
},
|
||||
"exec_err_state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error"
|
||||
]
|
||||
},
|
||||
"for": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string",
|
||||
@@ -5097,12 +5099,22 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"keep_firing_for": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"missing_series_evals_to_resolve": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"namespace_uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"no_data_state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -5111,12 +5123,18 @@
|
||||
"OK"
|
||||
]
|
||||
},
|
||||
"rule_group": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6239,6 +6257,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -6249,6 +6273,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -7569,6 +7599,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -7579,6 +7615,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -8243,6 +8285,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"RemoteWriteConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResponseDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8520,6 +8570,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -8530,6 +8586,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -152,67 +152,6 @@ func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req c
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// BatchCheck implements claims.AccessClient.
|
||||
func (c authzLimitedClient) BatchCheck(ctx context.Context, id claims.AuthInfo, req claims.BatchCheckRequest) (claims.BatchCheckResponse, error) {
|
||||
ctx, span := tracer.Start(ctx, "resource.authzLimitedClient.BatchCheck", trace.WithAttributes(
|
||||
attribute.Int("num_checks", len(req.Checks)),
|
||||
attribute.Bool("fallback_used", FallbackUsed(ctx)),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
if FallbackUsed(ctx) {
|
||||
span.SetStatus(codes.Error, "BatchCheck not supported with fallback")
|
||||
return claims.BatchCheckResponse{}, fmt.Errorf("BatchCheck not supported when fallback is used")
|
||||
}
|
||||
|
||||
// Filter checks to only those that require RBAC and validate namespace
|
||||
rbacChecks := make([]claims.BatchCheckItem, 0, len(req.Checks))
|
||||
allowedByDefault := make(map[string]bool, len(req.Checks))
|
||||
|
||||
for _, check := range req.Checks {
|
||||
if !claims.NamespaceMatches(id.GetNamespace(), check.Namespace) {
|
||||
span.SetStatus(codes.Error, "Namespace mismatch")
|
||||
span.RecordError(claims.ErrNamespaceMismatch)
|
||||
return claims.BatchCheckResponse{}, claims.ErrNamespaceMismatch
|
||||
}
|
||||
|
||||
if c.IsCompatibleWithRBAC(check.Group, check.Resource) {
|
||||
rbacChecks = append(rbacChecks, check)
|
||||
} else {
|
||||
allowedByDefault[check.CorrelationID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// If all checks are allowed by default, return early
|
||||
if len(rbacChecks) == 0 {
|
||||
results := make(map[string]claims.BatchCheckResult, len(req.Checks))
|
||||
for _, check := range req.Checks {
|
||||
results[check.CorrelationID] = claims.BatchCheckResult{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
||||
return claims.BatchCheckResponse{Results: results}, nil
|
||||
}
|
||||
|
||||
// Call the underlying client with RBAC checks
|
||||
resp, err := c.client.BatchCheck(ctx, id, claims.BatchCheckRequest{Checks: rbacChecks})
|
||||
if err != nil {
|
||||
c.logger.FromContext(ctx).Error("BatchCheck failed", "error", err, "num_checks", len(rbacChecks))
|
||||
span.SetStatus(codes.Error, fmt.Sprintf("batch check failed: %v", err))
|
||||
span.RecordError(err)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Merge results with allowed-by-default checks
|
||||
for correlationID := range allowedByDefault {
|
||||
resp.Results[correlationID] = claims.BatchCheckResult{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Compile implements claims.AccessClient.
|
||||
func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req claims.ListRequest) (claims.ItemChecker, claims.Zookie, error) {
|
||||
t := time.Now()
|
||||
|
||||
@@ -159,97 +159,6 @@ func TestNamespaceMatching(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthzLimitedClient_BatchCheck(t *testing.T) {
|
||||
mockClient := authlib.FixedAccessClient(true)
|
||||
client := NewAuthzLimitedClient(mockClient, AuthzOptions{})
|
||||
|
||||
t.Run("returns error when fallback is used", func(t *testing.T) {
|
||||
ctx := WithFallback(context.Background())
|
||||
req := authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{
|
||||
CorrelationID: "0",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: utils.VerbGet,
|
||||
Namespace: "stacks-1",
|
||||
Name: "test-dashboard",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.BatchCheck(ctx, &identity.StaticRequester{Namespace: "stacks-1"}, req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "fallback")
|
||||
})
|
||||
|
||||
t.Run("works normally without fallback", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
req := authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{
|
||||
CorrelationID: "0",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: utils.VerbGet,
|
||||
Namespace: "stacks-1",
|
||||
Name: "test-dashboard",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.BatchCheck(ctx, &identity.StaticRequester{Namespace: "stacks-1"}, req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Results, 1)
|
||||
assert.True(t, resp.Results["0"].Allowed)
|
||||
})
|
||||
|
||||
t.Run("returns error on namespace mismatch", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
req := authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{
|
||||
CorrelationID: "0",
|
||||
Group: "dashboard.grafana.app",
|
||||
Resource: "dashboards",
|
||||
Verb: utils.VerbGet,
|
||||
Namespace: "stacks-2", // Different namespace
|
||||
Name: "test-dashboard",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.BatchCheck(ctx, &identity.StaticRequester{Namespace: "stacks-1"}, req)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, authlib.ErrNamespaceMismatch)
|
||||
})
|
||||
|
||||
t.Run("allows non-RBAC resources by default", func(t *testing.T) {
|
||||
// Use a client that would deny if checked
|
||||
denyClient := authlib.FixedAccessClient(false)
|
||||
client := NewAuthzLimitedClient(denyClient, AuthzOptions{})
|
||||
|
||||
ctx := context.Background()
|
||||
req := authlib.BatchCheckRequest{
|
||||
Checks: []authlib.BatchCheckItem{
|
||||
{
|
||||
CorrelationID: "0",
|
||||
Group: "unknown.group",
|
||||
Resource: "unknown.resource",
|
||||
Verb: utils.VerbGet,
|
||||
Namespace: "stacks-1",
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.BatchCheck(ctx, &identity.StaticRequester{Namespace: "stacks-1"}, req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Results, 1)
|
||||
assert.True(t, resp.Results["0"].Allowed, "non-RBAC resources should be allowed by default")
|
||||
})
|
||||
}
|
||||
|
||||
// TestNamespaceMatchingFallback tests namespace matching in Check and Compile methods when fallback is used
|
||||
func TestNamespaceMatchingFallback(t *testing.T) {
|
||||
// Create a mock client that always returns allowed=true
|
||||
|
||||
@@ -33,8 +33,6 @@ import (
|
||||
)
|
||||
|
||||
func TestIntegrationFolderTreeZanzana(t *testing.T) {
|
||||
// TODO: Add back OSS seeding and enable this test
|
||||
t.Skip("Skipping folder tree test with Zanzana")
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
runIntegrationFolderTree(t, testinfra.GrafanaOpts{
|
||||
|
||||
76
public/api-merged.json
generated
76
public/api-merged.json
generated
@@ -13829,12 +13829,6 @@
|
||||
"BacktestConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -13844,8 +13838,16 @@
|
||||
"$ref": "#/definitions/AlertQuery"
|
||||
}
|
||||
},
|
||||
"exec_err_state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error"
|
||||
]
|
||||
},
|
||||
"for": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"type": "string",
|
||||
@@ -13854,12 +13856,22 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"keep_firing_for": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"missing_series_evals_to_resolve": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"namespace_uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"no_data_state": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -13868,12 +13880,18 @@
|
||||
"OK"
|
||||
]
|
||||
},
|
||||
"rule_group": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -16778,6 +16796,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -16788,6 +16812,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -19263,6 +19293,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -19273,6 +19309,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -20310,6 +20352,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"RemoteWriteConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -21009,6 +21059,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/definitions/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
@@ -21019,6 +21075,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteWriteConfig"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -70,8 +70,8 @@ export class PanelTimeRange extends SceneTimeRangeTransformerBase<PanelTimeRange
|
||||
// set initial values on activate
|
||||
this.setState({
|
||||
value: timeRange,
|
||||
from: timeRange.raw.from.toString(),
|
||||
to: timeRange.raw.to.toString(),
|
||||
from: typeof timeRange.raw.from === 'string' ? timeRange.raw.from : timeRange.raw.from.toISOString(),
|
||||
to: typeof timeRange.raw.to === 'string' ? timeRange.raw.to : timeRange.raw.to.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,5 +17,5 @@ export const usePluginConfig = (plugin?: CatalogPlugin) => {
|
||||
return loadPlugin(plugin.id);
|
||||
}
|
||||
return null;
|
||||
}, [plugin?.id, plugin?.isInstalled, plugin?.isDisabled]);
|
||||
}, [plugin?.id, plugin?.isInstalled, plugin?.isDisabled, plugin?.isFullyInstalled]);
|
||||
};
|
||||
|
||||
76
public/openapi3.json
generated
76
public/openapi3.json
generated
@@ -3364,12 +3364,6 @@
|
||||
},
|
||||
"BacktestConfig": {
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3379,8 +3373,16 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"exec_err_state": {
|
||||
"enum": [
|
||||
"OK",
|
||||
"Alerting",
|
||||
"Error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"for": {
|
||||
"$ref": "#/components/schemas/Duration"
|
||||
"type": "string"
|
||||
},
|
||||
"from": {
|
||||
"format": "date-time",
|
||||
@@ -3389,12 +3391,22 @@
|
||||
"interval": {
|
||||
"$ref": "#/components/schemas/Duration"
|
||||
},
|
||||
"keep_firing_for": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"missing_series_evals_to_resolve": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"namespace_uid": {
|
||||
"type": "string"
|
||||
},
|
||||
"no_data_state": {
|
||||
"enum": [
|
||||
"Alerting",
|
||||
@@ -3403,12 +3415,18 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"rule_group": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -6313,6 +6331,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/components/schemas/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -6323,6 +6347,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GettableExtendedRuleNode"
|
||||
@@ -8798,6 +8828,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/components/schemas/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -8808,6 +8844,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PostableExtendedRuleNode"
|
||||
@@ -9846,6 +9888,14 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RemoteWriteConfig": {
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Report": {
|
||||
"properties": {
|
||||
"created": {
|
||||
@@ -10544,6 +10594,12 @@
|
||||
"interval": {
|
||||
"$ref": "#/components/schemas/Duration"
|
||||
},
|
||||
"labels": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"limit": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
@@ -10554,6 +10610,12 @@
|
||||
"query_offset": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote_write": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RemoteWriteConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"rules": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GettableExtendedRuleNode"
|
||||
|
||||
Reference in New Issue
Block a user