use FF and cleanup access code

This commit is contained in:
konsalex
2025-11-26 11:41:58 +01:00
parent 3a9559c8d0
commit 90a830ad71
7 changed files with 113 additions and 97 deletions
-8
View File
@@ -27,13 +27,6 @@ storage_type = unified
; Configure dashboards to use unified storage
[unified_storage.dashboards.dashboard.grafana.app]
; Dualwriter modes:
; 0: disabled (default) - dashboards saved to SQL only
; 1: read from legacy, write to legacy, write to unified best-effort
; 2: read from legacy, write to both
; 3: read from unified, write to both
; 4: read from unified, write to unified (fully migrated)
; 5: read from unified, write to unified, ignore background sync state
dualWriterMode = 5
; Configure folders to use unified storage (required for dashboards)
@@ -44,5 +37,4 @@ dualWriterMode = 5
; SQLite database for testing
type = sqlite3
path = grafana.db
; Enable high availability mode is false for single instance
high_availability = false
+1 -1
View File
@@ -63,7 +63,7 @@ kubectl apply -f ./pkg/registry/apis/apiextensions/resources/example-widget.yaml
Or use curl:
```bash
curl -k -X POST https://localhost:1111/apis/apiextensions.k8s.io/v1/customresourcedefinitions \
curl -k -X POST https://localhost:1111/apis/customcrdtest.grafana.app/v1/namespaces/default/widgets \
-H "$AUTH_SVC" \
-H "Content-Type: application/yaml" \
--data-binary @$PWD/pkg/registry/apis/apiextensions/resources/example-widget.yaml
+2 -1
View File
@@ -73,7 +73,7 @@ func RegisterAPIService(
unified resource.ResourceClient,
) (*APIExtensionsBuilder, error) {
if !features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) {
return nil, fmt.Errorf("apiextensions feature flag is not enabled")
return nil, nil
}
b := &APIExtensionsBuilder{
@@ -188,6 +188,7 @@ func (b *APIExtensionsBuilder) loadAndRegisterCRDs(ctx context.Context, crdStore
// This allows us to list CRDs without a user session during server startup
// TODO(@konsalex): Does this cause any security issue? Not 100% how to authenticate
// a service call like this
// Use well-know constants, and co-ord with IAM and SnStorage to ensure this is secure.
systemCtx := resource.WithFallback(ctx)
systemCtx = authlib.WithAuthInfo(systemCtx, &identity.StaticRequester{
Type: authlib.TypeServiceAccount,
@@ -1,9 +1,8 @@
apiVersion: customcrdtest.grafana.app/v1
kind: Widget
metadata:
name: my-widget-2
name: my-widget
namespace: default
spec:
size: medium
replicas: 3
replicas: 3
+73
View File
@@ -0,0 +1,73 @@
package apiserver
import (
"net/http"
"strings"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
)
// createDynamicHandlerWrapper wraps the not-found handler with dynamic custom resource handlers
func (s *service) createDynamicHandlerWrapper(builders []builder.APIGroupBuilder, notFoundHandler http.Handler) http.Handler {
// Look for the APIExtensionsBuilder - we'll fetch the handler lazily on each request
// because the handler is created during UpdateAPIGroupInfo which happens AFTER this wrapper is installed
type dynamicHandlerProvider interface {
GetDynamicHandler() http.Handler
}
var dhProvider dynamicHandlerProvider
for _, b := range builders {
if provider, ok := b.(dynamicHandlerProvider); ok {
dhProvider = provider
break
}
}
if dhProvider == nil {
return notFoundHandler
}
// Return a wrapper that lazily fetches and tries the dynamic handler on each request
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if this is an /apis/ request that might be for a custom resource
if strings.HasPrefix(r.URL.Path, "/apis/") {
// Fetch the dynamic handler (it will be nil until UpdateAPIGroupInfo creates it)
dynamicHandler := dhProvider.GetDynamicHandler()
if dynamicHandler != nil {
// Create a response recorder to capture what the dynamic handler does
recorder := &responseRecorder{
ResponseWriter: w,
statusCode: 0,
}
dynamicHandler.ServeHTTP(recorder, r)
// If the dynamic handler handled it (didn't return 404), we're done
if recorder.statusCode != 0 && recorder.statusCode != http.StatusNotFound {
return
}
}
}
// Otherwise, fall back to the not-found handler
notFoundHandler.ServeHTTP(w, r)
})
}
// responseRecorder captures the status code from a handler
type responseRecorder struct {
http.ResponseWriter
statusCode int
}
func (r *responseRecorder) WriteHeader(statusCode int) {
r.statusCode = statusCode
r.ResponseWriter.WriteHeader(statusCode)
}
func (r *responseRecorder) Write(b []byte) (int, error) {
if r.statusCode == 0 {
r.statusCode = http.StatusOK
}
return r.ResponseWriter.Write(b)
}
+16 -80
View File
@@ -379,8 +379,12 @@ func (s *service) start(ctx context.Context) error {
notFoundHandler := notfoundhandler.New(s.codecs, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
// Wrap the not-found handler with dynamic custom resource handler
finalHandler := s.createDynamicHandlerWrapper(builders, notFoundHandler)
var finalHandler http.Handler = notFoundHandler
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) {
// Wrap the not-found handler with dynamic custom resource handler
finalHandler = s.createDynamicHandlerWrapper(builders, notFoundHandler)
}
if err := appinstaller.RegisterPostStartHooks(s.appInstallers, serverConfig); err != nil {
return fmt.Errorf("failed to register post start hooks for app installers: %w", err)
@@ -392,13 +396,16 @@ func (s *service) start(ctx context.Context) error {
return err
}
// Inject the server instance into any builders that need it (e.g., APIExtensionsBuilder for dynamic CRD registration)
type apiServerSetter interface {
SetAPIServer(server *genericapiserver.GenericAPIServer)
}
for _, b := range builders {
if setter, ok := b.(apiServerSetter); ok {
setter.SetAPIServer(server)
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagApiExtensions) {
// Inject the server instance into any builders that need it (e.g., APIExtensionsBuilder for dynamic CRD registration)
type apiServerSetter interface {
SetAPIServer(server *genericapiserver.GenericAPIServer)
}
for _, b := range builders {
if setter, ok := b.(apiServerSetter); ok {
setter.SetAPIServer(server)
}
}
}
@@ -510,77 +517,6 @@ func (s *service) start(ctx context.Context) error {
return nil
}
// createDynamicHandlerWrapper wraps the not-found handler with dynamic custom resource handlers
func (s *service) createDynamicHandlerWrapper(builders []builder.APIGroupBuilder, notFoundHandler http.Handler) http.Handler {
// Look for the APIExtensionsBuilder - we'll fetch the handler lazily on each request
// because the handler is created during UpdateAPIGroupInfo which happens AFTER this wrapper is installed
fmt.Printf("createDynamicHandlerWrapper: Setting up lazy handler wrapper for %d builders...\n", len(builders))
type dynamicHandlerProvider interface {
GetDynamicHandler() http.Handler
}
var dhProvider dynamicHandlerProvider
for i, b := range builders {
fmt.Printf(" Builder %d: %T\n", i, b)
if provider, ok := b.(dynamicHandlerProvider); ok {
fmt.Printf(" ✓ Builder %d implements dynamicHandlerProvider - will fetch handler lazily\n", i)
dhProvider = provider
break
}
}
if dhProvider == nil {
// No dynamic handler provider found, just use the not-found handler
fmt.Println(" No dynamic handler provider found, using standard not-found handler")
return notFoundHandler
}
// Return a wrapper that lazily fetches and tries the dynamic handler on each request
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if this is an /apis/ request that might be for a custom resource
if strings.HasPrefix(r.URL.Path, "/apis/") {
// Fetch the dynamic handler (it will be nil until UpdateAPIGroupInfo creates it)
dynamicHandler := dhProvider.GetDynamicHandler()
if dynamicHandler != nil {
// Create a response recorder to capture what the dynamic handler does
recorder := &responseRecorder{
ResponseWriter: w,
statusCode: 0,
}
dynamicHandler.ServeHTTP(recorder, r)
// If the dynamic handler handled it (didn't return 404), we're done
if recorder.statusCode != 0 && recorder.statusCode != http.StatusNotFound {
return
}
}
}
// Otherwise, fall back to the not-found handler
notFoundHandler.ServeHTTP(w, r)
})
}
// responseRecorder captures the status code from a handler
type responseRecorder struct {
http.ResponseWriter
statusCode int
}
func (r *responseRecorder) WriteHeader(statusCode int) {
r.statusCode = statusCode
r.ResponseWriter.WriteHeader(statusCode)
}
func (r *responseRecorder) Write(b []byte) (int, error) {
if r.statusCode == 0 {
r.statusCode = http.StatusOK
}
return r.ResponseWriter.Write(b)
}
func (s *service) startCoreServer(
ctx context.Context,
transport *grafanaapiserveroptions.RoundTripperFunc,
+19 -4
View File
@@ -135,8 +135,17 @@ func (c authzLimitedClient) Check(ctx context.Context, id claims.AuthInfo, req c
return claims.CheckResponse{Allowed: true}, nil
}
// For cluster-scoped resources (empty namespace), skip namespace matching check
if req.Namespace != "" && !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) {
// Hack, allow creation of Cluster scoped resources (ex. register CRDs)
// We need to make sure it is the correct service account,
// not just any service account.
// This is called when we submit a CRD (not when we list them)
// Currently creating them with a Service Account thus the match
if req.Namespace == "" && claims.IsIdentityType(id.GetIdentityType(), claims.TypeServiceAccount, claims.TypeAccessPolicy) {
span.SetAttributes(attribute.Bool("allowed", true))
return claims.CheckResponse{Allowed: true}, nil
}
if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) {
span.SetAttributes(attribute.Bool("allowed", false))
span.SetStatus(codes.Error, "Namespace mismatch")
span.RecordError(claims.ErrNamespaceMismatch)
@@ -185,8 +194,14 @@ func (c authzLimitedClient) Compile(ctx context.Context, id claims.AuthInfo, req
return true
}, claims.NoopZookie{}, nil
}
// For cluster-scoped resources (empty namespace), skip namespace matching check
if req.Namespace != "" && !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) {
// Hack, allow system tokens to be able to
// access Cluster scoped resources (ex. register CRDs)
// We need to make sure it is the correct service account,
// not just any service account
isServiceAccnt := req.Namespace == "" && claims.IsIdentityType(id.GetIdentityType(), claims.TypeAccessPolicy)
if !claims.NamespaceMatches(id.GetNamespace(), req.Namespace) && !isServiceAccnt {
span.SetAttributes(attribute.Bool("allowed", false))
span.SetStatus(codes.Error, "Namespace mismatch")
span.RecordError(claims.ErrNamespaceMismatch)