diff --git a/pkg/services/apiserver/appinstaller/installer.go b/pkg/services/apiserver/appinstaller/installer.go index 2824a731726..d80e865b5f0 100644 --- a/pkg/services/apiserver/appinstaller/installer.go +++ b/pkg/services/apiserver/appinstaller/installer.go @@ -86,6 +86,27 @@ func RegisterAdmissionPlugins( return nil } +type AuthorizerRegistrar interface { + Register(gv schema.GroupVersion, authorizer authorizer.Authorizer) +} + +func RegisterAuthorizers( + ctx context.Context, + appInstallers []appsdkapiserver.AppInstaller, + registrar AuthorizerRegistrar, +) { + logger := logging.FromContext(ctx) + for _, installer := range appInstallers { + if authorizerProvider, ok := installer.(AuthorizerProvider); ok { + authorizer := authorizerProvider.GetAuthorizer() + for _, gv := range installer.GroupVersions() { + registrar.Register(gv, authorizer) + logger.Debug("Registered authorizer", "group", gv.Group, "version", gv.Version, "app") + } + } + } +} + func BuildOpenAPIDefGetter( appInstallers []appsdkapiserver.AppInstaller, ) func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { diff --git a/pkg/services/apiserver/appinstaller/installer_test.go b/pkg/services/apiserver/appinstaller/installer_test.go new file mode 100644 index 00000000000..89c4bfa0164 --- /dev/null +++ b/pkg/services/apiserver/appinstaller/installer_test.go @@ -0,0 +1,143 @@ +package appinstaller + +import ( + "context" + "testing" + + appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestRegisterAuthorizers(t *testing.T) { + tests := []struct { + name string + appInstallers []appsdkapiserver.AppInstaller + expectedRegisters int + }{ + { + name: "empty installers list", + appInstallers: []appsdkapiserver.AppInstaller{}, + expectedRegisters: 0, + }, + { + name: "installer without authorizer provider", + appInstallers: []appsdkapiserver.AppInstaller{ + &mockAppInstaller{ + groupVersions: []schema.GroupVersion{ + {Group: "test.example.com", Version: "v1"}, + }, + }, + }, + expectedRegisters: 0, + }, + { + name: "single installer with authorizer provider", + appInstallers: []appsdkapiserver.AppInstaller{ + &mockAppInstallerWithAuth{ + mockAppInstaller: &mockAppInstaller{ + groupVersions: []schema.GroupVersion{ + {Group: "test.example.com", Version: "v1"}, + }, + }, + mockAuthorizer: &mockAuthorizer{}, + }, + }, + expectedRegisters: 1, + }, + { + name: "installer with multiple group versions", + appInstallers: []appsdkapiserver.AppInstaller{ + &mockAppInstallerWithAuth{ + mockAppInstaller: &mockAppInstaller{ + groupVersions: []schema.GroupVersion{ + {Group: "test.example.com", Version: "v1"}, + {Group: "test.example.com", Version: "v2"}, + {Group: "other.example.com", Version: "v1"}, + }, + }, + mockAuthorizer: &mockAuthorizer{}, + }, + }, + expectedRegisters: 3, + }, + { + name: "multiple installers with mixed authorizer support", + appInstallers: []appsdkapiserver.AppInstaller{ + &mockAppInstallerWithAuth{ + mockAppInstaller: &mockAppInstaller{ + groupVersions: []schema.GroupVersion{ + {Group: "test.example.com", Version: "v1"}, + }, + }, + mockAuthorizer: &mockAuthorizer{}, + }, + &mockAppInstaller{ + groupVersions: []schema.GroupVersion{ + {Group: "other.example.com", Version: "v1"}, + }, + }, + &mockAppInstallerWithAuth{ + mockAppInstaller: &mockAppInstaller{ + groupVersions: []schema.GroupVersion{ + {Group: "another.example.com", Version: "v1"}, + {Group: "another.example.com", Version: "v2"}, + }, + }, + mockAuthorizer: &mockAuthorizer{}, + }, + }, + expectedRegisters: 3, // 1 from first installer + 2 from third installer + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + registrar := &mockAuthorizerRegistrar{} + RegisterAuthorizers(ctx, tt.appInstallers, registrar) + require.Equal(t, tt.expectedRegisters, len(registrar.registrations)) + }) + } +} + +type mockAppInstaller struct { + appsdkapiserver.AppInstaller // Embed the interface + groupVersions []schema.GroupVersion +} + +func (m *mockAppInstaller) GroupVersions() []schema.GroupVersion { + return m.groupVersions +} + +type mockAppInstallerWithAuth struct { + *mockAppInstaller + mockAuthorizer authorizer.Authorizer +} + +func (m *mockAppInstallerWithAuth) GetAuthorizer() authorizer.Authorizer { + return m.mockAuthorizer +} + +type mockRegistration struct { + groupVersion schema.GroupVersion + authorizer authorizer.Authorizer +} + +type mockAuthorizerRegistrar struct { + registrations []mockRegistration +} + +func (m *mockAuthorizerRegistrar) Register(gv schema.GroupVersion, auth authorizer.Authorizer) { + m.registrations = append(m.registrations, mockRegistration{ + groupVersion: gv, + authorizer: auth, + }) +} + +type mockAuthorizer struct{} + +func (m *mockAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) { + return authorizer.DecisionAllow, "test", nil +} diff --git a/pkg/services/apiserver/service.go b/pkg/services/apiserver/service.go index 9fd8e263bbb..58bf5ad4ac7 100644 --- a/pkg/services/apiserver/service.go +++ b/pkg/services/apiserver/service.go @@ -292,6 +292,9 @@ func (s *service) start(ctx context.Context) error { return err } + // Register authorizers from app installers + appinstaller.RegisterAuthorizers(ctx, s.appInstallers, s.authorizer) + err = applyGrafanaConfig(s.cfg, s.features, o) if err != nil { return err