diff --git a/conf/apiextensions.ini b/conf/apiextensions.ini index 39884b5e656..747e034026f 100644 --- a/conf/apiextensions.ini +++ b/conf/apiextensions.ini @@ -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 \ No newline at end of file diff --git a/pkg/registry/apis/apiextensions/README.md b/pkg/registry/apis/apiextensions/README.md index 6a2f3d5a9a1..30a2a3ee860 100644 --- a/pkg/registry/apis/apiextensions/README.md +++ b/pkg/registry/apis/apiextensions/README.md @@ -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 diff --git a/pkg/registry/apis/apiextensions/register.go b/pkg/registry/apis/apiextensions/register.go index e6e95eea193..f046330559b 100644 --- a/pkg/registry/apis/apiextensions/register.go +++ b/pkg/registry/apis/apiextensions/register.go @@ -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, diff --git a/pkg/registry/apis/apiextensions/resources/example-widget.yaml b/pkg/registry/apis/apiextensions/resources/example-widget.yaml index 7b22240e12e..a171420c46a 100644 --- a/pkg/registry/apis/apiextensions/resources/example-widget.yaml +++ b/pkg/registry/apis/apiextensions/resources/example-widget.yaml @@ -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 \ No newline at end of file diff --git a/pkg/services/apiserver/dynamic.go b/pkg/services/apiserver/dynamic.go new file mode 100644 index 00000000000..e9f1c5e2818 --- /dev/null +++ b/pkg/services/apiserver/dynamic.go @@ -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) +} diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index d6235c02643..32f914e733b 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -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, diff --git a/pkg/storage/unified/resource/access.go b/pkg/storage/unified/resource/access.go index cafa972e7a3..b3e7b64c629 100644 --- a/pkg/storage/unified/resource/access.go +++ b/pkg/storage/unified/resource/access.go @@ -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)