mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Rendering: Experimental support to use JWTs as auth method (#60841)
* Rendering: Add support for auth through JWT * Goimports * Apply review suggestions * Correct feature toggle ref * Minor changes
This commit is contained in:
committed by
GitHub
parent
99ac39f0d2
commit
87a0c95164
@@ -50,6 +50,7 @@ Some stable features are enabled by default. You can disable a stable feature by
|
|||||||
| `accessControlOnCall` | Access control primitives for OnCall |
|
| `accessControlOnCall` | Access control primitives for OnCall |
|
||||||
| `alertingNoNormalState` | Stop maintaining state of alerts that are not firing |
|
| `alertingNoNormalState` | Stop maintaining state of alerts that are not firing |
|
||||||
| `disableElasticsearchBackendExploreQuery` | Disable executing of Elasticsearch Explore queries trough backend |
|
| `disableElasticsearchBackendExploreQuery` | Disable executing of Elasticsearch Explore queries trough backend |
|
||||||
|
| `renderAuthJWT` | Uses JWT-based auth for rendering instead of relying on remote cache |
|
||||||
|
|
||||||
## Alpha feature toggles
|
## Alpha feature toggles
|
||||||
|
|
||||||
|
|||||||
@@ -92,4 +92,5 @@ export interface FeatureToggles {
|
|||||||
alertStateHistoryLokiPrimary?: boolean;
|
alertStateHistoryLokiPrimary?: boolean;
|
||||||
alertStateHistoryLokiOnly?: boolean;
|
alertStateHistoryLokiOnly?: boolean;
|
||||||
unifiedRequestLog?: boolean;
|
unifiedRequestLog?: boolean;
|
||||||
|
renderAuthJWT?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ var (
|
|||||||
// MRenderingSummary is a metric summary for image rendering request duration
|
// MRenderingSummary is a metric summary for image rendering request duration
|
||||||
MRenderingSummary *prometheus.SummaryVec
|
MRenderingSummary *prometheus.SummaryVec
|
||||||
|
|
||||||
|
// MRenderingUserLookupSummary is a metric summary for image rendering user lookup duration
|
||||||
|
MRenderingUserLookupSummary *prometheus.SummaryVec
|
||||||
|
|
||||||
// MAccessPermissionsSummary is a metric summary for loading permissions request duration when evaluating access
|
// MAccessPermissionsSummary is a metric summary for loading permissions request duration when evaluating access
|
||||||
MAccessPermissionsSummary prometheus.Histogram
|
MAccessPermissionsSummary prometheus.Histogram
|
||||||
|
|
||||||
@@ -392,6 +395,16 @@ func init() {
|
|||||||
[]string{"status", "type"},
|
[]string{"status", "type"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MRenderingUserLookupSummary = prometheus.NewSummaryVec(
|
||||||
|
prometheus.SummaryOpts{
|
||||||
|
Name: "rendering_user_lookup_duration_milliseconds",
|
||||||
|
Help: "summary of rendering user lookup duration",
|
||||||
|
Objectives: objectiveMap,
|
||||||
|
Namespace: ExporterName,
|
||||||
|
},
|
||||||
|
[]string{"success", "from"},
|
||||||
|
)
|
||||||
|
|
||||||
MRenderingQueue = prometheus.NewGauge(prometheus.GaugeOpts{
|
MRenderingQueue = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "rendering_queue_size",
|
Name: "rendering_queue_size",
|
||||||
Help: "size of rendering queue",
|
Help: "size of rendering queue",
|
||||||
@@ -663,6 +676,7 @@ func initMetricVars() {
|
|||||||
LDAPUsersSyncExecutionTime,
|
LDAPUsersSyncExecutionTime,
|
||||||
MRenderingRequestTotal,
|
MRenderingRequestTotal,
|
||||||
MRenderingSummary,
|
MRenderingSummary,
|
||||||
|
MRenderingUserLookupSummary,
|
||||||
MRenderingQueue,
|
MRenderingQueue,
|
||||||
MAccessPermissionsSummary,
|
MAccessPermissionsSummary,
|
||||||
MAccessEvaluationsSummary,
|
MAccessEvaluationsSummary,
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ const (
|
|||||||
InvalidAPIKey = "invalid API key"
|
InvalidAPIKey = "invalid API key"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ServiceName = "ContextHandler"
|
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtService jwt.JWTService,
|
func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtService jwt.JWTService,
|
||||||
remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore db.DB,
|
remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore db.DB,
|
||||||
tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service,
|
tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service,
|
||||||
|
|||||||
@@ -494,5 +494,11 @@ var (
|
|||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
Owner: grafanaBackendPlatformSquad,
|
Owner: grafanaBackendPlatformSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "renderAuthJWT",
|
||||||
|
Description: "Uses JWT-based auth for rendering instead of relying on remote cache",
|
||||||
|
State: FeatureStateBeta,
|
||||||
|
Owner: grafanaAsCodeSquad,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -73,3 +73,4 @@ alertStateHistoryLokiSecondary,alpha,@grafana/alerting-squad,false,false,false,f
|
|||||||
alertStateHistoryLokiPrimary,alpha,@grafana/alerting-squad,false,false,false,false
|
alertStateHistoryLokiPrimary,alpha,@grafana/alerting-squad,false,false,false,false
|
||||||
alertStateHistoryLokiOnly,alpha,@grafana/alerting-squad,false,false,false,false
|
alertStateHistoryLokiOnly,alpha,@grafana/alerting-squad,false,false,false,false
|
||||||
unifiedRequestLog,alpha,@grafana/backend-platform,false,false,false,false
|
unifiedRequestLog,alpha,@grafana/backend-platform,false,false,false,false
|
||||||
|
renderAuthJWT,beta,@grafana/grafana-as-code,false,false,false,false
|
||||||
|
|||||||
|
@@ -302,4 +302,8 @@ const (
|
|||||||
// FlagUnifiedRequestLog
|
// FlagUnifiedRequestLog
|
||||||
// Writes error logs to the request logger
|
// Writes error logs to the request logger
|
||||||
FlagUnifiedRequestLog = "unifiedRequestLog"
|
FlagUnifiedRequestLog = "unifiedRequestLog"
|
||||||
|
|
||||||
|
// FlagRenderAuthJWT
|
||||||
|
// Uses JWT-based auth for rendering instead of relying on remote cache
|
||||||
|
FlagRenderAuthJWT = "renderAuthJWT"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,19 +26,63 @@ type RenderUser struct {
|
|||||||
OrgRole string `json:"org_role"`
|
OrgRole string `json:"org_role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type renderJWT struct {
|
||||||
|
RenderUser *RenderUser
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) {
|
func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) {
|
||||||
val, err := rs.RemoteCacheService.Get(ctx, fmt.Sprintf(renderKeyPrefix, key))
|
var from string
|
||||||
if err != nil {
|
start := time.Now()
|
||||||
rs.log.Error("Failed to get render key from cache", "error", err)
|
|
||||||
}
|
var renderUser *RenderUser
|
||||||
ru := &RenderUser{}
|
|
||||||
buf := bytes.NewBuffer(val)
|
if looksLikeJWT(key) && rs.features.IsEnabled(featuremgmt.FlagRenderAuthJWT) {
|
||||||
err = gob.NewDecoder(buf).Decode(&ru)
|
from = "jwt"
|
||||||
if err != nil {
|
renderUser = rs.getRenderUserFromJWT(key)
|
||||||
return nil, false
|
} else {
|
||||||
|
from = "cache"
|
||||||
|
renderUser = rs.getRenderUserFromCache(ctx, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ru, true
|
found := renderUser != nil
|
||||||
|
success := strconv.FormatBool(found)
|
||||||
|
metrics.MRenderingUserLookupSummary.WithLabelValues(success, from).Observe(float64(time.Since(start)))
|
||||||
|
|
||||||
|
return renderUser, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RenderingService) getRenderUserFromJWT(key string) *RenderUser {
|
||||||
|
claims := new(renderJWT)
|
||||||
|
tkn, err := jwt.ParseWithClaims(key, claims, func(_ *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(rs.Cfg.RendererAuthToken), nil
|
||||||
|
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS512.Alg()}))
|
||||||
|
|
||||||
|
if err != nil || !tkn.Valid {
|
||||||
|
rs.log.Error("Could not get render user from JWT", "err", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.RenderUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RenderingService) getRenderUserFromCache(ctx context.Context, key string) *RenderUser {
|
||||||
|
val, err := rs.RemoteCacheService.Get(ctx, fmt.Sprintf(renderKeyPrefix, key))
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Error("Could not get render user from remote cache", "err", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ru := new(RenderUser)
|
||||||
|
buf := bytes.NewBuffer(val)
|
||||||
|
|
||||||
|
err = gob.NewDecoder(buf).Decode(&ru)
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Error("Could not decode render user from remote cache", "err", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ru
|
||||||
}
|
}
|
||||||
|
|
||||||
func setRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts AuthOpts, renderKey string, expiry time.Duration) error {
|
func setRenderKey(cache *remotecache.RemoteCache, ctx context.Context, opts AuthOpts, renderKey string, expiry time.Duration) error {
|
||||||
@@ -103,7 +153,7 @@ func (r *perRequestRenderKeyProvider) get(ctx context.Context, opts AuthOpts) (s
|
|||||||
return generateAndSetRenderKey(r.cache, ctx, opts, r.keyExpiry)
|
return generateAndSetRenderKey(r.cache, ctx, opts, r.keyExpiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *perRequestRenderKeyProvider) afterRequest(ctx context.Context, opts AuthOpts, renderKey string) {
|
func (r *perRequestRenderKeyProvider) afterRequest(ctx context.Context, _ AuthOpts, renderKey string) {
|
||||||
deleteRenderKey(r.cache, r.log, ctx, renderKey)
|
deleteRenderKey(r.cache, r.log, ctx, renderKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +167,7 @@ func (r *longLivedRenderKeyProvider) get(ctx context.Context, opts AuthOpts) (st
|
|||||||
return r.renderKey, nil
|
return r.renderKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *longLivedRenderKeyProvider) afterRequest(ctx context.Context, opts AuthOpts, renderKey string) {
|
func (r *longLivedRenderKeyProvider) afterRequest(_ context.Context, _ AuthOpts, _ string) {
|
||||||
// do nothing - renderKey from longLivedRenderKeyProvider is deleted only after session expires
|
// do nothing - renderKey from longLivedRenderKeyProvider is deleted only after session expires
|
||||||
// or someone calls session.Dispose()
|
// or someone calls session.Dispose()
|
||||||
}
|
}
|
||||||
@@ -125,3 +175,35 @@ func (r *longLivedRenderKeyProvider) afterRequest(ctx context.Context, opts Auth
|
|||||||
func (r *longLivedRenderKeyProvider) Dispose(ctx context.Context) {
|
func (r *longLivedRenderKeyProvider) Dispose(ctx context.Context) {
|
||||||
deleteRenderKey(r.cache, r.log, ctx, r.renderKey)
|
deleteRenderKey(r.cache, r.log, ctx, r.renderKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type jwtRenderKeyProvider struct {
|
||||||
|
log log.Logger
|
||||||
|
authToken []byte
|
||||||
|
keyExpiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtRenderKeyProvider) get(_ context.Context, opts AuthOpts) (string, error) {
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS512, j.buildJWTClaims(opts))
|
||||||
|
return token.SignedString(j.authToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtRenderKeyProvider) buildJWTClaims(opts AuthOpts) renderJWT {
|
||||||
|
return renderJWT{
|
||||||
|
RenderUser: &RenderUser{
|
||||||
|
OrgID: opts.OrgID,
|
||||||
|
UserID: opts.UserID,
|
||||||
|
OrgRole: string(opts.OrgRole),
|
||||||
|
},
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(j.keyExpiry)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtRenderKeyProvider) afterRequest(_ context.Context, _ AuthOpts, _ string) {
|
||||||
|
// do nothing - the JWT will just expire
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeJWT(key string) bool {
|
||||||
|
return strings.HasPrefix(key, "eyJ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,14 +19,13 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Service = (*RenderingService)(nil)
|
var _ Service = (*RenderingService)(nil)
|
||||||
|
|
||||||
const ServiceName = "RenderingService"
|
|
||||||
|
|
||||||
type RenderingService struct {
|
type RenderingService struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
pluginInfo *plugins.Plugin
|
pluginInfo *plugins.Plugin
|
||||||
@@ -42,11 +41,12 @@ type RenderingService struct {
|
|||||||
|
|
||||||
perRequestRenderKeyProvider renderKeyProvider
|
perRequestRenderKeyProvider renderKeyProvider
|
||||||
Cfg *setting.Cfg
|
Cfg *setting.Cfg
|
||||||
|
features *featuremgmt.FeatureManager
|
||||||
RemoteCacheService *remotecache.RemoteCache
|
RemoteCacheService *remotecache.RemoteCache
|
||||||
RendererPluginManager plugins.RendererManager
|
RendererPluginManager plugins.RendererManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm plugins.RendererManager) (*RenderingService, error) {
|
func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager, remoteCache *remotecache.RemoteCache, rm plugins.RendererManager) (*RenderingService, error) {
|
||||||
// ensure ImagesDir exists
|
// ensure ImagesDir exists
|
||||||
err := os.MkdirAll(cfg.ImagesDir, 0700)
|
err := os.MkdirAll(cfg.ImagesDir, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -83,12 +83,23 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p
|
|||||||
domain = "localhost"
|
domain = "localhost"
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &RenderingService{
|
var renderKeyProvider renderKeyProvider
|
||||||
perRequestRenderKeyProvider: &perRequestRenderKeyProvider{
|
if features.IsEnabled(featuremgmt.FlagRenderAuthJWT) {
|
||||||
|
renderKeyProvider = &jwtRenderKeyProvider{
|
||||||
|
log: logger,
|
||||||
|
authToken: []byte(cfg.RendererAuthToken),
|
||||||
|
keyExpiry: cfg.RendererRenderKeyLifeTime,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderKeyProvider = &perRequestRenderKeyProvider{
|
||||||
cache: remoteCache,
|
cache: remoteCache,
|
||||||
log: logger,
|
log: logger,
|
||||||
keyExpiry: cfg.RendererRenderKeyLifeTime,
|
keyExpiry: cfg.RendererRenderKeyLifeTime,
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &RenderingService{
|
||||||
|
perRequestRenderKeyProvider: renderKeyProvider,
|
||||||
capabilities: []Capability{
|
capabilities: []Capability{
|
||||||
{
|
{
|
||||||
name: FullHeightImages,
|
name: FullHeightImages,
|
||||||
@@ -104,6 +115,7 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
|
features: features,
|
||||||
RemoteCacheService: remoteCache,
|
RemoteCacheService: remoteCache,
|
||||||
RendererPluginManager: rm,
|
RendererPluginManager: rm,
|
||||||
log: logger,
|
log: logger,
|
||||||
|
|||||||
Reference in New Issue
Block a user