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 |
| `alertingNoNormalState` | Stop maintaining state of alerts that are not firing |
| `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

View File

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

View File

@@ -122,6 +122,9 @@ var (
// MRenderingSummary is a metric summary for image rendering request duration
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 prometheus.Histogram
@@ -392,6 +395,16 @@ func init() {
[]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{
Name: "rendering_queue_size",
Help: "size of rendering queue",
@@ -663,6 +676,7 @@ func initMetricVars() {
LDAPUsersSyncExecutionTime,
MRenderingRequestTotal,
MRenderingSummary,
MRenderingUserLookupSummary,
MRenderingQueue,
MAccessPermissionsSummary,
MAccessEvaluationsSummary,

View File

@@ -46,8 +46,6 @@ const (
InvalidAPIKey = "invalid API key"
)
const ServiceName = "ContextHandler"
func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtService jwt.JWTService,
remoteCache *remotecache.RemoteCache, renderService rendering.Service, sqlStore db.DB,
tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service,

View File

@@ -494,5 +494,11 @@ var (
State: FeatureStateAlpha,
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
alertStateHistoryLokiOnly,alpha,@grafana/alerting-squad,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
// Writes error logs to the request logger
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"
"encoding/gob"
"fmt"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"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/services/featuremgmt"
"github.com/grafana/grafana/pkg/util"
)
@@ -20,19 +26,63 @@ type RenderUser struct {
OrgRole string `json:"org_role"`
}
type renderJWT struct {
RenderUser *RenderUser
jwt.RegisteredClaims
}
func (rs *RenderingService) GetRenderUser(ctx context.Context, key string) (*RenderUser, bool) {
val, err := rs.RemoteCacheService.Get(ctx, fmt.Sprintf(renderKeyPrefix, key))
if err != nil {
rs.log.Error("Failed to get render key from cache", "error", err)
}
ru := &RenderUser{}
buf := bytes.NewBuffer(val)
err = gob.NewDecoder(buf).Decode(&ru)
if err != nil {
return nil, false
var from string
start := time.Now()
var renderUser *RenderUser
if looksLikeJWT(key) && rs.features.IsEnabled(featuremgmt.FlagRenderAuthJWT) {
from = "jwt"
renderUser = rs.getRenderUserFromJWT(key)
} 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 {
@@ -103,7 +153,7 @@ func (r *perRequestRenderKeyProvider) get(ctx context.Context, opts AuthOpts) (s
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)
}
@@ -117,7 +167,7 @@ func (r *longLivedRenderKeyProvider) get(ctx context.Context, opts AuthOpts) (st
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
// 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) {
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/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var _ Service = (*RenderingService)(nil)
const ServiceName = "RenderingService"
type RenderingService struct {
log log.Logger
pluginInfo *plugins.Plugin
@@ -42,11 +41,12 @@ type RenderingService struct {
perRequestRenderKeyProvider renderKeyProvider
Cfg *setting.Cfg
features *featuremgmt.FeatureManager
RemoteCacheService *remotecache.RemoteCache
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
err := os.MkdirAll(cfg.ImagesDir, 0700)
if err != nil {
@@ -83,12 +83,23 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p
domain = "localhost"
}
s := &RenderingService{
perRequestRenderKeyProvider: &perRequestRenderKeyProvider{
var renderKeyProvider renderKeyProvider
if features.IsEnabled(featuremgmt.FlagRenderAuthJWT) {
renderKeyProvider = &jwtRenderKeyProvider{
log: logger,
authToken: []byte(cfg.RendererAuthToken),
keyExpiry: cfg.RendererRenderKeyLifeTime,
}
} else {
renderKeyProvider = &perRequestRenderKeyProvider{
cache: remoteCache,
log: logger,
keyExpiry: cfg.RendererRenderKeyLifeTime,
},
}
}
s := &RenderingService{
perRequestRenderKeyProvider: renderKeyProvider,
capabilities: []Capability{
{
name: FullHeightImages,
@@ -104,6 +115,7 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm p
},
},
Cfg: cfg,
features: features,
RemoteCacheService: remoteCache,
RendererPluginManager: rm,
log: logger,