mirror of
https://github.com/grafana/grafana.git
synced 2026-06-09 00:23:05 -04:00
DataSources: Load secure values from decrypter (#124515)
This commit is contained in:
parent
abcfb01704
commit
29939bcd79
15 changed files with 564 additions and 106 deletions
|
|
@ -159,7 +159,6 @@ require (
|
|||
github.com/go-openapi/validate v0.25.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
|
|
@ -183,6 +182,7 @@ require (
|
|||
github.com/grafana/grafana-aws-sdk v1.4.4 // indirect
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.4.0 // indirect
|
||||
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
|
||||
github.com/grafana/grafana/apps/secret v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/infra/features v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0 // indirect
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ require (
|
|||
github.com/grafana/grafana/apps/folder v0.0.0 // indirect
|
||||
github.com/grafana/grafana/apps/iam v0.0.0 // indirect
|
||||
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
|
||||
github.com/grafana/grafana/apps/secret v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/infra/features v0.0.0 // indirect
|
||||
github.com/grafana/grafana/pkg/plugins v0.0.0 // indirect
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ type PluginProxy struct {
|
|||
matchedRoute *plugins.Route
|
||||
dataProxyLogging bool // from cfg
|
||||
sendUserHeader bool // from cfg
|
||||
secureJsonData pluginsettings.SecureJsonGetter
|
||||
secureJsonData pluginsettings.DecryptedSecureJSONLoader
|
||||
tracer tracing.Tracer
|
||||
transport *http.Transport
|
||||
features featuremgmt.FeatureToggles
|
||||
|
|
@ -44,7 +44,7 @@ func NewPluginProxy(ps *pluginsettings.DTO, routes []*plugins.Route,
|
|||
r *http.Request, w http.ResponseWriter, signedInUser identity.Requester,
|
||||
proxyPath string,
|
||||
dataProxyLogging bool, sendUserHeader bool,
|
||||
secureJsonData pluginsettings.SecureJsonGetter, tracer tracing.Tracer,
|
||||
secureJsonData pluginsettings.DecryptedSecureJSONLoader, tracer tracing.Tracer,
|
||||
transport *http.Transport, accessControl ac.AccessControl, features featuremgmt.FeatureToggles) (*PluginProxy, error) {
|
||||
return &PluginProxy{
|
||||
accessControl: accessControl,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
_ "github.com/dlmiddlecote/sqlstats"
|
||||
_ "github.com/dolthub/go-mysql-server"
|
||||
_ "github.com/dolthub/go-mysql-server/sql"
|
||||
_ "github.com/dolthub/go-mysql-server/sql/analyzer"
|
||||
_ "github.com/dolthub/go-mysql-server/sql/expression"
|
||||
_ "github.com/dolthub/go-mysql-server/sql/expression/function/aggregation"
|
||||
_ "github.com/dolthub/go-mysql-server/sql/plan"
|
||||
|
|
|
|||
|
|
@ -9,31 +9,13 @@ import (
|
|||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
apppluginV0 "github.com/grafana/grafana/pkg/apis/appplugin/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
)
|
||||
|
||||
type ctxAppDTO struct{}
|
||||
|
||||
type shimDTO struct {
|
||||
getDecryptedSecureJSONData pluginsettings.SecureJsonGetter
|
||||
}
|
||||
|
||||
// This can be removed when we no longer support loading directly from the legacy SQL store
|
||||
func withShimDTO(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, ctxAppDTO{}, &shimDTO{})
|
||||
}
|
||||
|
||||
func legacyShimFromContext(ctx context.Context) *shimDTO {
|
||||
shim, ok := ctx.Value(ctxAppDTO{}).(*shimDTO)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return shim
|
||||
}
|
||||
|
||||
func (b *AppPluginAPIBuilder) getSettings(ctx context.Context) (*apppluginV0.Settings, pluginsettings.SecureJsonGetter, error) {
|
||||
ctx = withShimDTO(ctx)
|
||||
func (b *AppPluginAPIBuilder) getSettings(ctx context.Context) (*apppluginV0.Settings, pluginsettings.DecryptedSecureJSONLoader, error) {
|
||||
ctx = pluginsettings.WithSecureContextShim(ctx)
|
||||
raw, err := b.getter.Get(ctx, apppluginV0.INSTANCE_NAME, &v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
@ -47,44 +29,16 @@ func (b *AppPluginAPIBuilder) getSettings(ctx context.Context) (*apppluginV0.Set
|
|||
}
|
||||
|
||||
if len(settings.Secure) < 1 {
|
||||
return settings, func(ctx context.Context) (map[string]string, error) { return map[string]string{}, nil }, nil
|
||||
return settings, pluginsettings.EmptyDecryptedSecureJSONLoader, nil
|
||||
}
|
||||
|
||||
shim := legacyShimFromContext(ctx)
|
||||
if shim != nil && shim.getDecryptedSecureJSONData != nil {
|
||||
return settings, shim.getDecryptedSecureJSONData, nil
|
||||
obj, err := utils.MetaAccessor(settings)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Returns settings and a function to get decrypted secure values
|
||||
return settings, func(ctx context.Context) (map[string]string, error) {
|
||||
names := make([]string, 0, len(settings.Secure))
|
||||
for k, v := range settings.Secure {
|
||||
if v.Name == "" {
|
||||
return nil, fmt.Errorf("missing secure value name for key: %s", k)
|
||||
}
|
||||
names = append(names, v.Name)
|
||||
}
|
||||
lookup, err := b.decrypter.Decrypt(ctx, b.groupVersion.Group, settings.Namespace, names...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decrypting secure values: %w", err)
|
||||
}
|
||||
|
||||
decrypted := make(map[string]string)
|
||||
for k, sv := range settings.Secure {
|
||||
v, ok := lookup[sv.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find secure value: %s for key: %s", sv.Name, k)
|
||||
}
|
||||
if v.Error() != nil {
|
||||
return nil, fmt.Errorf("error decrypting secure value: %s / %w", k, v.Error())
|
||||
}
|
||||
val := v.Value()
|
||||
if val != nil {
|
||||
decrypted[k] = val.DangerouslyExposeAndConsumeValue()
|
||||
}
|
||||
}
|
||||
return decrypted, nil
|
||||
}, nil
|
||||
loader, err := pluginsettings.GetDecryptedSecureJSONLoader(ctx, obj, b.decrypter)
|
||||
return settings, loader, err
|
||||
}
|
||||
|
||||
// Gets plugin context with decrypted secure values
|
||||
|
|
|
|||
|
|
@ -151,13 +151,9 @@ func (s *settingsStorage) get(ctx context.Context) (*apppluginV0.Settings, error
|
|||
return nil, fmt.Errorf("failed to get plugin settings: %w", err)
|
||||
}
|
||||
if ps != nil {
|
||||
shim := legacyShimFromContext(ctx)
|
||||
if shim != nil {
|
||||
shim.getDecryptedSecureJSONData = func(ctx context.Context) (map[string]string, error) {
|
||||
v := s.pluginSettings.DecryptedValues(ps)
|
||||
return v, nil // odd this does not have an error
|
||||
}
|
||||
}
|
||||
pluginsettings.WithDecryptedValues(ctx, func(ctx context.Context) (map[string]string, error) {
|
||||
return s.pluginSettings.DecryptedValues(ps), nil
|
||||
})
|
||||
|
||||
obj.SetCreationTimestamp(metav1.NewTime(ps.Updated))
|
||||
obj.SetResourceVersion(getLegacySettingsResourceVersion(ps))
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import (
|
|||
type subProxyREST struct {
|
||||
pluginID string
|
||||
routes []*plugins.Route
|
||||
settingsProvider func(ctx context.Context) (*apppluginV0.Settings, pluginsettings.SecureJsonGetter, error)
|
||||
settingsProvider func(ctx context.Context) (*apppluginV0.Settings, pluginsettings.DecryptedSecureJSONLoader, error)
|
||||
accessControl ac.AccessControl
|
||||
tracer tracing.Tracer
|
||||
features featuremgmt.FeatureToggles
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
|
|
@ -178,6 +179,11 @@ func (q *scopedDatasourceProvider) GetDataSource(ctx context.Context, uid string
|
|||
}
|
||||
}
|
||||
|
||||
// Add the decrypted secrets to the context if they were requested
|
||||
pluginsettings.WithDecryptedValues(ctx, func(ctx context.Context) (map[string]string, error) {
|
||||
return secrets, nil
|
||||
})
|
||||
|
||||
return q.converter.AsDataSource(ds)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -15,13 +16,14 @@ import (
|
|||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
openapi "k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/pluginschema"
|
||||
"github.com/grafana/grafana/apps/secret/pkg/decrypt"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
|
|
@ -56,11 +58,16 @@ type DataSourceAPIBuilder struct {
|
|||
client PluginClient // will only ever be called with the same plugin id!
|
||||
datasources PluginDatasourceProvider
|
||||
contextProvider PluginContextWrapper
|
||||
accessControl accesscontrol.AccessControl
|
||||
decrypter decrypt.DecryptService // when not reading legacy
|
||||
accessControl accesscontrol.AccessControl // ST Only
|
||||
accessClient authlib.AccessClient // MT+ST
|
||||
schemas map[string]*pluginschema.PluginSchema
|
||||
queryTypes *datasourceV0.QueryTypeDefinitionList
|
||||
cfg DataSourceAPIBuilderConfig
|
||||
dataSourceCRUDMetric *prometheus.HistogramVec
|
||||
|
||||
// Legacy or Unified -- depending on config
|
||||
store grafanarest.Storage
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
|
|
@ -69,7 +76,9 @@ func RegisterAPIService(
|
|||
pluginClient plugins.Client, // access to everything
|
||||
datasources ScopedPluginDatasourceProvider,
|
||||
contextProvider PluginContextWrapper,
|
||||
decrypter decrypt.DecryptService, // when not reading legacy
|
||||
accessControl accesscontrol.AccessControl,
|
||||
accessClient authlib.AccessClient,
|
||||
reg prometheus.Registerer,
|
||||
pluginSources sources.Registry,
|
||||
) (*DataSourceAPIBuilder, error) {
|
||||
|
|
@ -124,6 +133,7 @@ func RegisterAPIService(
|
|||
datasources.GetDatasourceProvider(plugin.JSONData),
|
||||
contextProvider,
|
||||
accessControl,
|
||||
decrypter,
|
||||
flags,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -136,6 +146,7 @@ func RegisterAPIService(
|
|||
if plugin.Schemas != nil {
|
||||
builder.schemas = plugin.Schemas
|
||||
}
|
||||
builder.accessClient = accessClient // Only registered in ST for now
|
||||
|
||||
apiRegistrar.RegisterAPI(builder)
|
||||
}
|
||||
|
|
@ -159,6 +170,7 @@ func NewDataSourceAPIBuilder(
|
|||
datasources PluginDatasourceProvider,
|
||||
contextProvider PluginContextWrapper,
|
||||
accessControl accesscontrol.AccessControl,
|
||||
decrypter decrypt.DecryptService, // when not reading legacy
|
||||
cfg DataSourceAPIBuilderConfig,
|
||||
) (*DataSourceAPIBuilder, error) {
|
||||
registerSubresourceMetrics(prometheus.DefaultRegisterer)
|
||||
|
|
@ -170,6 +182,7 @@ func NewDataSourceAPIBuilder(
|
|||
datasources: datasources,
|
||||
contextProvider: contextProvider,
|
||||
accessControl: accessControl,
|
||||
decrypter: decrypter,
|
||||
cfg: cfg,
|
||||
}
|
||||
return builder, nil
|
||||
|
|
@ -263,15 +276,16 @@ func (b *DataSourceAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storage[ds.StoragePath()], err = opts.DualWriteBuilder(ds.GroupResource(), legacyStore, unified)
|
||||
b.store, err = opts.DualWriteBuilder(ds.GroupResource(), legacyStore, unified)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
storage[ds.StoragePath()] = b.store
|
||||
storage[ds.StoragePath("access")] = &subAccessREST{
|
||||
builder: b,
|
||||
getter: legacyStore,
|
||||
}
|
||||
} else {
|
||||
// Read only datasources
|
||||
storage[ds.StoragePath()] = &connectionAccess{
|
||||
datasources: b.datasources,
|
||||
resourceInfo: ds,
|
||||
|
|
@ -348,7 +362,15 @@ func (b *DataSourceAPIBuilder) getPluginContext(ctx context.Context, uid string)
|
|||
defer span.End()
|
||||
|
||||
getInstanceCtx, getInstanceSpan := tracing.Start(ctx, "datasource.getPluginContext.getInstanceSettings")
|
||||
instance, err := b.datasources.GetInstanceSettings(getInstanceCtx, uid)
|
||||
var err error
|
||||
var instance *backend.DataSourceInstanceSettings
|
||||
if b.store != nil && b.decrypter != nil {
|
||||
// Load from storage + decrypter (respecting dual write settings)
|
||||
instance, err = b.getInstanceSettings(getInstanceCtx, uid)
|
||||
} else {
|
||||
// This is backed by the datasources abstraction, NOT storage
|
||||
instance, err = b.datasources.GetInstanceSettings(getInstanceCtx, uid)
|
||||
}
|
||||
getInstanceSpan.End()
|
||||
if err != nil {
|
||||
err = tracing.Error(span, err)
|
||||
|
|
|
|||
72
pkg/registry/apis/datasource/settings.go
Normal file
72
pkg/registry/apis/datasource/settings.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
datasourceV0 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
)
|
||||
|
||||
func (b *DataSourceAPIBuilder) getInstanceSettings(ctx context.Context, name string) (*backend.DataSourceInstanceSettings, error) {
|
||||
ctx = pluginsettings.WithSecureContextShim(ctx)
|
||||
raw, err := b.store.Get(ctx, name, &v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ds, ok := raw.(*datasourceV0.DataSource)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type %T when getting plugin settings", raw)
|
||||
}
|
||||
|
||||
obj, err := utils.MetaAccessor(ds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts, _ := obj.GetUpdatedTimestamp()
|
||||
if ts == nil {
|
||||
ts = ptr.To(obj.GetCreationTimestamp().Time)
|
||||
}
|
||||
|
||||
gvk := obj.GetGroupVersionKind()
|
||||
if gvk.Version == "" {
|
||||
gvk.Version = datasourceV0.VERSION
|
||||
}
|
||||
|
||||
settings := &backend.DataSourceInstanceSettings{
|
||||
UID: ds.Name,
|
||||
Type: b.pluginJSON.ID,
|
||||
URL: ds.Spec.URL(),
|
||||
ID: obj.GetDeprecatedInternalID(), // nolint:staticcheck
|
||||
Name: ds.Spec.Title(),
|
||||
User: ds.Spec.User(),
|
||||
Database: ds.Spec.Database(),
|
||||
BasicAuthEnabled: ds.Spec.BasicAuth(),
|
||||
BasicAuthUser: ds.Spec.BasicAuthUser(),
|
||||
Updated: *ts,
|
||||
APIVersion: gvk.Version,
|
||||
}
|
||||
|
||||
settings.JSONData, err = json.Marshal(ds.Spec.JSONData())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ds.Secure) < 1 {
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
loader, err := pluginsettings.GetDecryptedSecureJSONLoader(ctx, obj, b.decrypter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings.DecryptedSecureJSONData, err = loader(ctx)
|
||||
return settings, err
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import (
|
|||
|
||||
type subAccessREST struct {
|
||||
builder *DataSourceAPIBuilder
|
||||
getter rest.Getter
|
||||
}
|
||||
|
||||
var _ = rest.Connecter(&subAccessREST{})
|
||||
|
|
@ -25,7 +24,7 @@ func (r *subAccessREST) New() runtime.Object {
|
|||
}
|
||||
|
||||
func (r *subAccessREST) Destroy() {
|
||||
// no-op implemenation needed for rest.Storage interface.
|
||||
// no-op implementation needed for rest.Storage interface.
|
||||
}
|
||||
|
||||
func (r *subAccessREST) ConnectMethods() []string {
|
||||
|
|
|
|||
56
pkg/server/wire_gen.go
generated
56
pkg/server/wire_gen.go
generated
|
|
@ -907,7 +907,17 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
|||
apiService := api3.ProvideService(cfg, routeRegisterImpl, accessControl, userimplService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
dashboardActivityChannel := live.ProvideDashboardActivityChannel(grafanaLive)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(featureToggles, apiserverService, dashboardService, service13, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, registerer, sqlStore, tracingService, resourceClient, dualwriteService, quotaService, eventualRestConfigProvider, userimplService, libraryElementService, v4, serviceImpl, dashboardActivityChannel, configProvider)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, pluginsourcesService)
|
||||
v10 := _wireValue
|
||||
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, v10)
|
||||
decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, decryptStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, decryptService, accessControl, accessClient, registerer, pluginsourcesService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -936,19 +946,9 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
|||
}
|
||||
collectionsAPIBuilder := collections.RegisterAPIService(cfg, featureToggles, sqlStore, starService, userimplService, apiserverService)
|
||||
webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer)
|
||||
v10 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
|
||||
v11 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
|
||||
pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer)
|
||||
v11 := extras.ProvideExtraWorkers(pullRequestWorker)
|
||||
v12 := _wireValue
|
||||
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, v12)
|
||||
decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, decryptStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v12 := extras.ProvideExtraWorkers(pullRequestWorker)
|
||||
factory := github.ProvideFactory()
|
||||
v13 := extras.ProvideProvisioningOSSRepositoryExtras(cfg, decryptService, factory, webhookExtraBuilder, registerer)
|
||||
repositoryFactory, err := extras.ProvideFactoryFromConfig(cfg, v13)
|
||||
|
|
@ -962,7 +962,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
|||
return nil, err
|
||||
}
|
||||
quotaGetter := extras.ProvideQuotaGetter(cfg)
|
||||
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, dualwriteService, usageStats, tracingService, v10, v11, repositoryFactory, connectionFactory, quotaGetter)
|
||||
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, dualwriteService, usageStats, tracingService, v11, v12, repositoryFactory, connectionFactory, quotaGetter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1619,7 +1619,17 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
|||
apiService := api3.ProvideService(cfg, routeRegisterImpl, accessControl, userimplService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService)
|
||||
dashboardActivityChannel := live.ProvideDashboardActivityChannel(grafanaLive)
|
||||
dashboardsAPIBuilder := dashboard.RegisterAPIService(featureToggles, apiserverService, dashboardService, service13, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, registerer, sqlStore, tracingService, resourceClient, dualwriteService, quotaService, eventualRestConfigProvider, userimplService, libraryElementService, v4, serviceImpl, dashboardActivityChannel, configProvider)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, pluginsourcesService)
|
||||
v10 := _wireValue
|
||||
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, v10)
|
||||
decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, decryptStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, decryptService, accessControl, accessClient, registerer, pluginsourcesService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1648,19 +1658,9 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
|||
}
|
||||
collectionsAPIBuilder := collections.RegisterAPIService(cfg, featureToggles, sqlStore, starService, userimplService, apiserverService)
|
||||
webhookExtraBuilder := webhooks.ProvideWebhooksWithImages(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer)
|
||||
v10 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
|
||||
v11 := extras.ProvideProvisioningExtraAPIs(webhookExtraBuilder)
|
||||
pullRequestWorker := pullrequest.ProvidePullRequestWorker(cfg, renderingService, resourceClient, eventualRestConfigProvider, registerer)
|
||||
v11 := extras.ProvideExtraWorkers(pullRequestWorker)
|
||||
v12 := _wireValue
|
||||
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, v12)
|
||||
decryptStorage, err := metadata.ProvideDecryptStorage(tracer, ossKeeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, registerer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decryptService, err := decrypt.ProvideDecryptService(cfg, tracer, decryptStorage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v12 := extras.ProvideExtraWorkers(pullRequestWorker)
|
||||
factory := github.ProvideFactory()
|
||||
v13 := extras.ProvideProvisioningOSSRepositoryExtras(cfg, decryptService, factory, webhookExtraBuilder, registerer)
|
||||
repositoryFactory, err := extras.ProvideFactoryFromConfig(cfg, v13)
|
||||
|
|
@ -1674,7 +1674,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
|||
return nil, err
|
||||
}
|
||||
quotaGetter := extras.ProvideQuotaGetter(cfg)
|
||||
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, dualwriteService, usageStats, tracingService, v10, v11, repositoryFactory, connectionFactory, quotaGetter)
|
||||
provisioningAPIBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, accessClient, dualwriteService, usageStats, tracingService, v11, v12, repositoryFactory, connectionFactory, quotaGetter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
98
pkg/services/pluginsintegration/pluginsettings/decrypted.go
Normal file
98
pkg/services/pluginsintegration/pluginsettings/decrypted.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package pluginsettings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/apps/secret/pkg/decrypt"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
|
||||
// DecryptedSecureJSONLoader returns the decrypted secure values for a resource
|
||||
// once preceding access checks have passed. Construction validates inputs;
|
||||
// the returned function performs the actual decryption on demand.
|
||||
type DecryptedSecureJSONLoader func(context.Context) (map[string]string, error)
|
||||
|
||||
// EmptyDecryptedSecureJSONLoader is a loader that returns no secure values.
|
||||
// Use it when a resource is known to have nothing to decrypt.
|
||||
func EmptyDecryptedSecureJSONLoader(context.Context) (map[string]string, error) {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
type ctxKey struct{}
|
||||
|
||||
type secureContextShim struct {
|
||||
loader DecryptedSecureJSONLoader
|
||||
}
|
||||
|
||||
// WithSecureContextShim must wrap a context before any request that may stash
|
||||
// already-decrypted values via WithDecryptedValues. Without the shim,
|
||||
// WithDecryptedValues is a no-op and GetDecryptedSecureJSONLoader falls through
|
||||
// to the decrypter.
|
||||
func WithSecureContextShim(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, ctxKey{}, &secureContextShim{})
|
||||
}
|
||||
|
||||
// WithDecryptedValues stashes already-decrypted secure values on the context
|
||||
// shim so a later GetDecryptedSecureJSONLoader call returns them directly
|
||||
// without re-decrypting. No-op if WithSecureContextShim was not called.
|
||||
func WithDecryptedValues(ctx context.Context, loader DecryptedSecureJSONLoader) {
|
||||
if shim, ok := ctx.Value(ctxKey{}).(*secureContextShim); ok {
|
||||
shim.loader = loader
|
||||
}
|
||||
}
|
||||
|
||||
// GetDecryptedSecureJSONLoader returns a loader for the decrypted secure values
|
||||
// of obj. If WithDecryptedValues populated the same context, those values are
|
||||
// reused; otherwise the loader calls decrypter on demand.
|
||||
func GetDecryptedSecureJSONLoader(ctx context.Context, obj utils.GrafanaMetaAccessor, decrypter decrypt.DecryptService) (DecryptedSecureJSONLoader, error) {
|
||||
if shim, ok := ctx.Value(ctxKey{}).(*secureContextShim); ok && shim.loader != nil {
|
||||
return shim.loader, nil
|
||||
}
|
||||
|
||||
secure, err := obj.GetSecureValues()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(secure) == 0 {
|
||||
return EmptyDecryptedSecureJSONLoader, nil
|
||||
}
|
||||
if decrypter == nil {
|
||||
return nil, errors.New("no decrypter configured")
|
||||
}
|
||||
|
||||
// Validate and collect names up front so config errors surface here rather
|
||||
// than on the first decrypt call.
|
||||
names := make([]string, 0, len(secure))
|
||||
for k, ref := range secure {
|
||||
if ref.Name == "" {
|
||||
return nil, fmt.Errorf("missing secure value name for key: %s", k)
|
||||
}
|
||||
names = append(names, ref.Name)
|
||||
}
|
||||
|
||||
group := obj.GetGroupVersionKind().Group
|
||||
namespace := obj.GetNamespace()
|
||||
return func(ctx context.Context) (map[string]string, error) {
|
||||
lookup, err := decrypter.Decrypt(ctx, group, namespace, names...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decrypting secure values: %w", err)
|
||||
}
|
||||
|
||||
decrypted := make(map[string]string, len(secure))
|
||||
for k, ref := range secure {
|
||||
res, ok := lookup[ref.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find secure value: %s for key: %s", ref.Name, k)
|
||||
}
|
||||
if err := res.Error(); err != nil {
|
||||
return nil, fmt.Errorf("error decrypting secure value: %s / %w", k, err)
|
||||
}
|
||||
if val := res.Value(); val != nil {
|
||||
decrypted[k] = val.DangerouslyExposeAndConsumeValue()
|
||||
}
|
||||
}
|
||||
return decrypted, nil
|
||||
}, nil
|
||||
}
|
||||
313
pkg/services/pluginsintegration/pluginsettings/decrypted_test.go
Normal file
313
pkg/services/pluginsintegration/pluginsettings/decrypted_test.go
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
package pluginsettings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1"
|
||||
"github.com/grafana/grafana/apps/secret/pkg/decrypt"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
|
||||
const testAPIVersion = "something.grafana.app/v7beta6"
|
||||
|
||||
func TestEmptyDecryptedSecureJSONLoader(t *testing.T) {
|
||||
out, err := EmptyDecryptedSecureJSONLoader(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
require.NotNil(t, out)
|
||||
}
|
||||
|
||||
func TestSecureContextShim(t *testing.T) {
|
||||
t.Run("WithDecryptedValues without shim is a no-op", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
// Should not panic; later GetDecryptedSecureJSONLoader must fall through
|
||||
// to the decrypter.
|
||||
WithDecryptedValues(ctx, func(context.Context) (map[string]string, error) {
|
||||
t.Fatal("loader should not be invoked")
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", nil)
|
||||
loader, err := GetDecryptedSecureJSONLoader(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
// No secure values configured, so we get the empty loader.
|
||||
out, err := loader(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
})
|
||||
|
||||
t.Run("WithSecureContextShim + WithDecryptedValues short-circuits decrypter", func(t *testing.T) {
|
||||
ctx := WithSecureContextShim(context.Background())
|
||||
|
||||
stashed := map[string]string{"token": "abc"}
|
||||
WithDecryptedValues(ctx, func(context.Context) (map[string]string, error) {
|
||||
return stashed, nil
|
||||
})
|
||||
|
||||
// Even with a decrypter and secure values present, the stashed loader wins.
|
||||
dec := &stubDecrypter{}
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "secret-token"},
|
||||
})
|
||||
loader, err := GetDecryptedSecureJSONLoader(ctx, obj, dec)
|
||||
require.NoError(t, err)
|
||||
out, err := loader(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, stashed, out)
|
||||
require.Zero(t, dec.calls, "decrypter must not be called when values were already stashed")
|
||||
})
|
||||
|
||||
t.Run("WithSecureContextShim without WithDecryptedValues falls through", func(t *testing.T) {
|
||||
ctx := WithSecureContextShim(context.Background())
|
||||
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", nil)
|
||||
loader, err := GetDecryptedSecureJSONLoader(ctx, obj, nil)
|
||||
require.NoError(t, err)
|
||||
out, err := loader(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDecryptedSecureJSONLoader(t *testing.T) {
|
||||
t.Run("no secure values returns empty loader", func(t *testing.T) {
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{})
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), obj, nil)
|
||||
require.NoError(t, err)
|
||||
out, err := loader(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
})
|
||||
|
||||
t.Run("error reading secure values is returned", func(t *testing.T) {
|
||||
// Set 'secure' to an unsupported type so GetSecureValues fails.
|
||||
u := &unstructured.Unstructured{Object: map[string]any{
|
||||
"metadata": map[string]any{"namespace": "ns"},
|
||||
"secure": 42,
|
||||
}}
|
||||
obj, err := utils.MetaAccessor(u)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = GetDecryptedSecureJSONLoader(context.Background(), obj, &stubDecrypter{})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("secure values present but no decrypter errors", func(t *testing.T) {
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "secret-token"},
|
||||
})
|
||||
_, err := GetDecryptedSecureJSONLoader(context.Background(), obj, nil)
|
||||
require.ErrorContains(t, err, "no decrypter configured")
|
||||
})
|
||||
|
||||
t.Run("missing secure value name surfaces during construction", func(t *testing.T) {
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: ""},
|
||||
})
|
||||
_, err := GetDecryptedSecureJSONLoader(context.Background(), obj, &stubDecrypter{})
|
||||
require.ErrorContains(t, err, "missing secure value name")
|
||||
require.ErrorContains(t, err, "token")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDecryptLoader_Success(t *testing.T) {
|
||||
val := secretv1beta1.NewExposedSecureValue("super-secret")
|
||||
dec := &stubDecrypter{
|
||||
results: map[string]decrypt.DecryptResult{
|
||||
"secret-token": decrypt.NewDecryptResultValue(&val),
|
||||
},
|
||||
}
|
||||
|
||||
obj := newTestObject(t, "myns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "secret-token"},
|
||||
})
|
||||
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), obj, dec)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := loader(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{"token": "super-secret"}, out)
|
||||
|
||||
require.Equal(t, 1, dec.calls)
|
||||
require.Equal(t, "something.grafana.app", dec.gotGroup)
|
||||
require.Equal(t, "myns", dec.gotNamespace)
|
||||
require.Equal(t, []string{"secret-token"}, dec.gotNames)
|
||||
}
|
||||
|
||||
func TestDecryptLoader_MultipleValues(t *testing.T) {
|
||||
tokenVal := secretv1beta1.NewExposedSecureValue("token-v")
|
||||
pwVal := secretv1beta1.NewExposedSecureValue("pw-v")
|
||||
dec := &stubDecrypter{
|
||||
results: map[string]decrypt.DecryptResult{
|
||||
"name-token": decrypt.NewDecryptResultValue(&tokenVal),
|
||||
"name-password": decrypt.NewDecryptResultValue(&pwVal),
|
||||
},
|
||||
}
|
||||
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "name-token"},
|
||||
"password": {Name: "name-password"},
|
||||
})
|
||||
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), obj, dec)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := loader(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{
|
||||
"token": "token-v",
|
||||
"password": "pw-v",
|
||||
}, out)
|
||||
|
||||
// Both ref names were forwarded; map iteration order isn't stable, so
|
||||
// compare as a set.
|
||||
require.ElementsMatch(t, []string{"name-token", "name-password"}, dec.gotNames)
|
||||
}
|
||||
|
||||
func TestDecryptLoader_DecrypterErrorPropagated(t *testing.T) {
|
||||
dec := &stubDecrypter{err: errors.New("boom")}
|
||||
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "secret-token"},
|
||||
})
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), obj, dec)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = loader(context.Background())
|
||||
require.ErrorContains(t, err, "error decrypting secure values")
|
||||
require.ErrorContains(t, err, "boom")
|
||||
}
|
||||
|
||||
func TestDecryptLoader_MissingResultForName(t *testing.T) {
|
||||
dec := &stubDecrypter{
|
||||
// Decrypter returns no entry for the requested name.
|
||||
results: map[string]decrypt.DecryptResult{},
|
||||
}
|
||||
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "secret-token"},
|
||||
})
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), obj, dec)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = loader(context.Background())
|
||||
require.ErrorContains(t, err, "unable to find secure value")
|
||||
require.ErrorContains(t, err, "secret-token")
|
||||
require.ErrorContains(t, err, "token")
|
||||
}
|
||||
|
||||
func TestDecryptLoader_PerValueErrorPropagated(t *testing.T) {
|
||||
dec := &stubDecrypter{
|
||||
results: map[string]decrypt.DecryptResult{
|
||||
"secret-token": decrypt.NewDecryptResultErr(errors.New("forbidden")),
|
||||
},
|
||||
}
|
||||
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "secret-token"},
|
||||
})
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), obj, dec)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = loader(context.Background())
|
||||
require.ErrorContains(t, err, "error decrypting secure value")
|
||||
require.ErrorContains(t, err, "token")
|
||||
require.ErrorContains(t, err, "forbidden")
|
||||
}
|
||||
|
||||
func TestDecryptLoader_NilValueIsSkipped(t *testing.T) {
|
||||
// A successful result with no value attached should not appear in the
|
||||
// decrypted map (and must not panic on DangerouslyExposeAndConsumeValue).
|
||||
dec := &stubDecrypter{
|
||||
results: map[string]decrypt.DecryptResult{
|
||||
"secret-token": decrypt.NewDecryptResultValue(nil),
|
||||
},
|
||||
}
|
||||
|
||||
obj := newTestObject(t, "ns", testAPIVersion, "DataSource", common.InlineSecureValues{
|
||||
"token": {Name: "secret-token"},
|
||||
})
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), obj, dec)
|
||||
require.NoError(t, err)
|
||||
|
||||
out, err := loader(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
}
|
||||
|
||||
func TestDecryptLoader_GroupAndNamespaceFromTypedObject(t *testing.T) {
|
||||
// Use a typed runtime.Object so GetGroupVersionKind takes the runtime path.
|
||||
val := secretv1beta1.NewExposedSecureValue("v")
|
||||
dec := &stubDecrypter{
|
||||
results: map[string]decrypt.DecryptResult{
|
||||
"n": decrypt.NewDecryptResultValue(&val),
|
||||
},
|
||||
}
|
||||
|
||||
u := &unstructured.Unstructured{}
|
||||
u.SetGroupVersionKind(schema.GroupVersionKind{Group: "g.example.com", Version: "v1", Kind: "Thing"})
|
||||
u.SetNamespace("ns-1")
|
||||
u.Object["secure"] = common.InlineSecureValues{"k": {Name: "n"}}
|
||||
|
||||
meta, err := utils.MetaAccessor(u)
|
||||
require.NoError(t, err)
|
||||
|
||||
loader, err := GetDecryptedSecureJSONLoader(context.Background(), meta, dec)
|
||||
require.NoError(t, err)
|
||||
_, err = loader(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "g.example.com", dec.gotGroup)
|
||||
require.Equal(t, "ns-1", dec.gotNamespace)
|
||||
}
|
||||
|
||||
// stubDecrypter is a test-only DecryptService that records its inputs and
|
||||
// returns canned results.
|
||||
type stubDecrypter struct {
|
||||
gotGroup string
|
||||
gotNamespace string
|
||||
gotNames []string
|
||||
calls int
|
||||
results map[string]decrypt.DecryptResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *stubDecrypter) Decrypt(_ context.Context, group, namespace string, names ...string) (map[string]decrypt.DecryptResult, error) {
|
||||
s.calls++
|
||||
s.gotGroup = group
|
||||
s.gotNamespace = namespace
|
||||
s.gotNames = append([]string(nil), names...)
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return s.results, nil
|
||||
}
|
||||
|
||||
func newTestObject(t *testing.T, namespace string, apiVersion string, kind string, secure common.InlineSecureValues) utils.GrafanaMetaAccessor {
|
||||
t.Helper()
|
||||
obj := map[string]any{
|
||||
"apiVersion": apiVersion,
|
||||
"kind": kind,
|
||||
"metadata": map[string]any{
|
||||
"namespace": namespace,
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"hello": "world",
|
||||
},
|
||||
}
|
||||
if secure != nil {
|
||||
// Store as the typed map; GetSecureValues handles the direct cast path.
|
||||
obj["secure"] = secure
|
||||
}
|
||||
u := &unstructured.Unstructured{Object: obj}
|
||||
meta, err := utils.MetaAccessor(u)
|
||||
require.NoError(t, err)
|
||||
return meta
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package pluginsettings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
|
@ -10,9 +9,6 @@ var (
|
|||
ErrPluginSettingNotFound = errors.New("plugin setting not found")
|
||||
)
|
||||
|
||||
// Get access to the decrypted secure values after other checks have passed
|
||||
type SecureJsonGetter func(context.Context) (map[string]string, error)
|
||||
|
||||
type DTO struct {
|
||||
ID int64
|
||||
OrgID int64
|
||||
|
|
|
|||
Loading…
Reference in a new issue