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:
Joan López de la Franca Beltran
2023-04-03 18:53:38 +02:00
committed by GitHub
parent 99ac39f0d2
commit 87a0c95164
9 changed files with 139 additions and 20 deletions

View File

@@ -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

View File

@@ -92,4 +92,5 @@ export interface FeatureToggles {
alertStateHistoryLokiPrimary?: boolean; alertStateHistoryLokiPrimary?: boolean;
alertStateHistoryLokiOnly?: boolean; alertStateHistoryLokiOnly?: boolean;
unifiedRequestLog?: boolean; unifiedRequestLog?: boolean;
renderAuthJWT?: boolean;
} }

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
},
} }
) )

View File

@@ -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
1 Name State Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
73 alertStateHistoryLokiPrimary alpha @grafana/alerting-squad false false false false
74 alertStateHistoryLokiOnly alpha @grafana/alerting-squad false false false false
75 unifiedRequestLog alpha @grafana/backend-platform false false false false
76 renderAuthJWT beta @grafana/grafana-as-code false false false false

View File

@@ -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"
) )

View File

@@ -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")
}

View File

@@ -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,