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 *shadowClientMetrics } // 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 }