Files
grafana/pkg/services/authz/zanzana/client/shadow_client.go
T
Alexander Zobnin 030c7099cb Zanzana: Fix shadow client context (#114853)
* Zanzana: Fix shadow client context

* don't cancel on parent context cancel

* share timeout
2025-12-04 17:09:04 +01:00

135 lines
4.0 KiB
Go

package client
import (
"context"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
authlib "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/infra/log"
)
var _ authlib.AccessClient = (*ShadowClient)(nil)
const zanzanaTimeout = 30 * time.Second
type ShadowClient struct {
logger log.Logger
accessClient authlib.AccessClient
zanzanaClient authlib.AccessClient
metrics *metrics
}
// WithShadowClient returns a new access client that runs zanzana checks in the background.
func WithShadowClient(accessClient authlib.AccessClient, zanzanaClient authlib.AccessClient, reg prometheus.Registerer) (authlib.AccessClient, error) {
client := &ShadowClient{
logger: log.New("zanzana-shadow-client"),
accessClient: accessClient,
zanzanaClient: zanzanaClient,
metrics: newShadowClientMetrics(reg),
}
return client, nil
}
func (c *ShadowClient) Check(ctx context.Context, id authlib.AuthInfo, req authlib.CheckRequest, folder string) (authlib.CheckResponse, error) {
acResChan := make(chan authlib.CheckResponse, 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.Check(zanzanaCtxTimeout, id, req, folder)
if err != nil {
c.logger.Error("Failed to run zanzana check", "error", err)
}
timer.ObserveDuration()
acRes := <-acResChan
acErr := <-acErrChan
if acErr == nil {
if res.Allowed != acRes.Allowed {
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
c.logger.Warn("Zanzana check result does not match", "expected", acRes.Allowed, "actual", res.Allowed, "user", id.GetUID(), "request", req)
} else {
c.metrics.evaluationStatusTotal.WithLabelValues("success").Inc()
}
}
}()
timer := prometheus.NewTimer(c.metrics.evaluationsSeconds.WithLabelValues("rbac"))
res, err := c.accessClient.Check(ctx, id, req, folder)
timer.ObserveDuration()
acResChan <- res
acErrChan <- err
return res, err
}
func (c *ShadowClient) Compile(ctx context.Context, id authlib.AuthInfo, req authlib.ListRequest) (authlib.ItemChecker, authlib.Zookie, error) {
zanzanaItemCheckerChan := make(chan authlib.ItemChecker, 1)
var zanzanaItemChecker authlib.ItemChecker
var once sync.Once
go func() {
if c.zanzanaClient == nil {
zanzanaItemCheckerChan <- nil
return
}
zanzanaCtx := context.WithoutCancel(ctx)
zanzanaCtxTimeout, cancel := context.WithTimeout(zanzanaCtx, zanzanaTimeout)
defer cancel()
timer := prometheus.NewTimer(c.metrics.compileSeconds.WithLabelValues("zanzana"))
itemChecker, _, err := c.zanzanaClient.Compile(zanzanaCtxTimeout, id, req)
timer.ObserveDuration()
if err != nil {
c.logger.Warn("Failed to compile zanzana item checker", "error", err)
}
zanzanaItemCheckerChan <- itemChecker
}()
timer := prometheus.NewTimer(c.metrics.compileSeconds.WithLabelValues("rbac"))
rbacItemChecker, _, err := c.accessClient.Compile(ctx, id, req)
timer.ObserveDuration()
if err != nil {
return nil, authlib.NoopZookie{}, err
}
shadowItemChecker := func(name, folder string) bool {
rbacRes := rbacItemChecker(name, folder)
go func() {
// Wait for zanzana result to be ready and then use it to compare against RBAC
once.Do(func() {
zanzanaItemChecker = <-zanzanaItemCheckerChan
})
if zanzanaItemChecker != nil {
zanzanaRes := zanzanaItemChecker(name, folder)
if zanzanaRes != rbacRes {
c.metrics.evaluationStatusTotal.WithLabelValues("error").Inc()
c.logger.Warn("Zanzana compile result does not match", "expected", rbacRes, "actual", zanzanaRes, "name", name, "folder", folder)
} else {
c.metrics.evaluationStatusTotal.WithLabelValues("success").Inc()
}
}
}()
return rbacRes
}
return shadowItemChecker, authlib.NoopZookie{}, err
}