From 62cc0f9c0e3f26d87204ba824279b79f0876146e Mon Sep 17 00:00:00 2001 From: Mihai Turdean <6640685+mihai-turdean@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:56:23 -0600 Subject: [PATCH] Udate IAM Folder Reconciler Operator config (#110728) --- apps/iam/cmd/operator/config.go | 2 +- go.mod | 1 - go.sum | 2 - pkg/operators/iam/README.md | 12 +- .../iam/zanzana_folder_reconciler.go | 143 +++++++++++------- pkg/services/authz/zanzana.go | 6 +- 6 files changed, 96 insertions(+), 70 deletions(-) diff --git a/apps/iam/cmd/operator/config.go b/apps/iam/cmd/operator/config.go index 0794b3fd496..2784c961a3e 100644 --- a/apps/iam/cmd/operator/config.go +++ b/apps/iam/cmd/operator/config.go @@ -109,7 +109,7 @@ func LoadConfigFromEnv() (*Config, error) { cfg.KubeConfig = kubeConfig } - cfg.ZanzanaClient.Address = os.Getenv("ZANZANA_ADDR") + cfg.ZanzanaClient.URL = os.Getenv("ZANZANA_ADDR") cfg.ZanzanaClient.Token = os.Getenv("ZANZANA_TOKEN") cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("TOKEN_EXCHANGE_URL") cfg.ZanzanaClient.ServerCertFile = os.Getenv("ZANZANA_SERVER_CERT_FILE") diff --git a/go.mod b/go.mod index 649998bacdf..8e5cdc08481 100644 --- a/go.mod +++ b/go.mod @@ -98,7 +98,6 @@ require ( github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend github.com/grafana/grafana-app-sdk v0.40.3 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-app-sdk/logging v0.40.3 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana-app-sdk/plugin v0.40.3 // @grafana/grafana-app-platform-squad github.com/grafana/grafana-aws-sdk v1.1.0 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad diff --git a/go.sum b/go.sum index 0d6155913f5..a9f74536af2 100644 --- a/go.sum +++ b/go.sum @@ -1605,8 +1605,6 @@ github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhck github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk= github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE= github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU= -github.com/grafana/grafana-app-sdk/plugin v0.40.3 h1:uH0oFZnYOUL+OXcyhd5NVYwoM+Wa0WUXvZ2Om1M91r0= -github.com/grafana/grafana-app-sdk/plugin v0.40.3/go.mod h1:+ylwE0P8WgPu5zURK5aDnVJpwRpuK3573rwrVV28qzQ= github.com/grafana/grafana-aws-sdk v1.1.0 h1:G0fvwbQmHw14c5RXPd7Gnw9ZQcgzl139LtMDoe0KhmE= github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8= github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= diff --git a/pkg/operators/iam/README.md b/pkg/operators/iam/README.md index 6fb3b088099..f1f245c6551 100644 --- a/pkg/operators/iam/README.md +++ b/pkg/operators/iam/README.md @@ -2,11 +2,13 @@ To build the operator, simply run `make build-go` To run the folder reconciler, you need a `./conf/operator.ini` config file. For example: ``` -[iam_folder_reconciler] -folder_app_url = https://host.docker.internal:6446 -folder_app_namespace = * -zanzana_address = zanzana.default.svc.cluster.local:50051 +[grpc_client_authentication] +token = IamFolderReconcilerToken token_exchange_url = http://host.docker.internal:8080/v1/sign-access-token -token = ProvisioningAdminToken + +[operator] +folder_app_url = https://host.docker.internal:6446 +zanzana_url = zanzana.default.svc.cluster.local:50051 +tls_insecure = true ``` After that, you can run it using: `GF_DEFAULT_TARGET=operator GF_OPERATOR_NAME=iam-folder-reconciler ./bin/linux-arm64/grafana server target --config=conf/operator.ini`. Beware that you will also need a TokenExchanger, a Zanzana Server and a Folder app running for the operator to behave. diff --git a/pkg/operators/iam/zanzana_folder_reconciler.go b/pkg/operators/iam/zanzana_folder_reconciler.go index fcaf42dc32f..c49088e54cc 100644 --- a/pkg/operators/iam/zanzana_folder_reconciler.go +++ b/pkg/operators/iam/zanzana_folder_reconciler.go @@ -2,6 +2,7 @@ package iam import ( "context" + "crypto/x509" "errors" "fmt" "log/slog" @@ -10,9 +11,9 @@ import ( "os/signal" "syscall" - "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-app-sdk/operator" + folder "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" "github.com/grafana/grafana/apps/iam/pkg/app" "github.com/grafana/grafana/pkg/server" "github.com/grafana/grafana/pkg/services/apiserver/standalone" @@ -22,7 +23,6 @@ import ( "k8s.io/client-go/transport" "github.com/grafana/authlib/authn" - "github.com/grafana/grafana-app-sdk/plugin/kubeconfig" utilnet "k8s.io/apimachinery/pkg/util/net" ) @@ -78,64 +78,63 @@ type iamConfig struct { AppConfig app.AppConfig } -const ( - ConnTypeGRPC = "grpc" - ConnTypeHTTP = "http" -) - func buildIAMConfigFromSettings(cfg *setting.Cfg) (*iamConfig, error) { - var err error if cfg == nil { return nil, fmt.Errorf("no configuration available") } iamCfg := iamConfig{} - iamFolderReconcilerSec := cfg.SectionWithEnvOverrides("iam_folder_reconciler") - - zanzanaAddress := iamFolderReconcilerSec.Key("zanzana_address").MustString("") - if zanzanaAddress == "" { - return nil, fmt.Errorf("address is required in [iam_folder_reconciler.zanzana] section") - } - iamCfg.AppConfig.ZanzanaClientCfg.Address = zanzanaAddress - - tokenExchangeURL := iamFolderReconcilerSec.Key("token_exchange_url").MustString("") - if tokenExchangeURL == "" { - return nil, fmt.Errorf("token_exchange_url is required in [iam_folder_reconciler] section") - } - iamCfg.AppConfig.ZanzanaClientCfg.TokenExchangeURL = tokenExchangeURL - - token := iamFolderReconcilerSec.Key("token").MustString("") + gRPCAuth := cfg.SectionWithEnvOverrides("grpc_client_authentication") + token := gRPCAuth.Key("token").String() if token == "" { - return nil, fmt.Errorf("token is required in [iam_folder_reconciler] section") + return nil, fmt.Errorf("token is required in [grpc_client_authentication] section") } iamCfg.AppConfig.ZanzanaClientCfg.Token = token - folderAppURL := iamFolderReconcilerSec.Key("folder_app_url").MustString("") - folderAppNamespace := iamFolderReconcilerSec.Key("folder_app_namespace").MustString("default") + tokenExchangeURL := gRPCAuth.Key("token_exchange_url").String() + if tokenExchangeURL == "" { + return nil, fmt.Errorf("token_exchange_url is required in [grpc_client_authentication] section") + } + iamCfg.AppConfig.ZanzanaClientCfg.TokenExchangeURL = tokenExchangeURL - kubeConfig, err := buildKubeConfigFromFolderAppURL(folderAppURL, tokenExchangeURL, token, folderAppNamespace) + operatorSec := cfg.SectionWithEnvOverrides("operator") + + zanzanaURL := operatorSec.Key("zanzana_url").MustString("") + if zanzanaURL == "" { + return nil, fmt.Errorf("zanzana_url is required in [operator] section") + } + iamCfg.AppConfig.ZanzanaClientCfg.URL = zanzanaURL + + folderAppURL := operatorSec.Key("folder_app_url").MustString("") + if folderAppURL == "" { + return nil, fmt.Errorf("folder_app_url is required in [operator] section") + } + + tlsInsecure := operatorSec.Key("tls_insecure").MustBool(false) + tlsCertFile := operatorSec.Key("tls_cert_file").String() + tlsKeyFile := operatorSec.Key("tls_key_file").String() + tlsCAFile := operatorSec.Key("tls_ca_file").String() + iamCfg.AppConfig.ZanzanaClientCfg.ServerCertFile = tlsCertFile + + kubeConfig, err := buildKubeConfigFromFolderAppURL( + folderAppURL, + tokenExchangeURL, token, + tlsInsecure, tlsCertFile, tlsKeyFile, tlsCAFile, + ) if err != nil { return nil, fmt.Errorf("failed to build kube config: %w", err) } - iamCfg.RunnerConfig.KubeConfig = kubeConfig.RestConfig - - wenhookSection := cfg.SectionWithEnvOverrides("iam_folder_reconciler.webhook_server") - webhookPort := wenhookSection.Key("port").MustInt(8443) - webhookCertPath := wenhookSection.Key("cert_path").MustString("") - webhookKeyPath := wenhookSection.Key("key_path").MustString("") - iamCfg.RunnerConfig.WebhookConfig = operator.RunnerWebhookConfig{ - Port: webhookPort, - TLSConfig: k8s.TLSConfig{ - CertPath: webhookCertPath, - KeyPath: webhookKeyPath, - }, - } + iamCfg.RunnerConfig.KubeConfig = *kubeConfig return &iamCfg, nil } -func buildKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, namespace string) (*kubeconfig.NamespacedConfig, error) { +func buildKubeConfigFromFolderAppURL( + folderAppURL string, + exchangeUrl, authToken string, + tlsInsecure bool, tlsCertFile, tlsKeyFile, tlsCAFile string, +) (*rest.Config, error) { tokenExchangeClient, err := authn.NewTokenExchangeClient(authn.TokenExchangeConfig{ TokenExchangeURL: exchangeUrl, Token: authToken, @@ -144,24 +143,53 @@ func buildKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, names return nil, fmt.Errorf("failed to create token exchange client: %w", err) } - return &kubeconfig.NamespacedConfig{ - RestConfig: rest.Config{ - APIPath: "/apis", - Host: folderAppURL, - WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper { - return &authRoundTripper{ - tokenExchangeClient: tokenExchangeClient, - transport: rt, - } - }), - TLSClientConfig: rest.TLSClientConfig{ - Insecure: true, - }, - }, - Namespace: namespace, + tlsConfig, err := buildTLSConfig(tlsInsecure, tlsCertFile, tlsKeyFile, tlsCAFile) + if err != nil { + return nil, fmt.Errorf("failed to build TLS configuration: %w", err) + } + + return &rest.Config{ + APIPath: "/apis", + Host: folderAppURL, + WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper { + return &authRoundTripper{ + tokenExchangeClient: tokenExchangeClient, + transport: rt, + } + }), + TLSClientConfig: tlsConfig, }, nil } +func buildTLSConfig(insecure bool, certFile, keyFile, caFile string) (rest.TLSClientConfig, error) { + tlsConfig := rest.TLSClientConfig{ + Insecure: insecure, + } + + if certFile != "" && keyFile != "" { + tlsConfig.CertFile = certFile + tlsConfig.KeyFile = keyFile + } + + if caFile != "" { + // caFile is set in operator.ini file + // nolint:gosec + caCert, err := os.ReadFile(caFile) + if err != nil { + return tlsConfig, fmt.Errorf("failed to read CA certificate file: %w", err) + } + + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return tlsConfig, fmt.Errorf("failed to parse CA certificate") + } + + tlsConfig.CAData = caCert + } + + return tlsConfig, nil +} + type authRoundTripper struct { tokenExchangeClient *authn.TokenExchangeClient transport http.RoundTripper @@ -169,7 +197,7 @@ type authRoundTripper struct { func (t *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { tokenResponse, err := t.tokenExchangeClient.Exchange(req.Context(), authn.TokenExchangeRequest{ - Audiences: []string{"folder.grafana.app"}, + Audiences: []string{folder.GROUP}, Namespace: "*", }) if err != nil { @@ -178,7 +206,6 @@ func (t *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) // clone the request as RTs are not expected to mutate the passed request req = utilnet.CloneRequest(req) - req.Header.Set("X-Access-Token", "Bearer "+tokenResponse.Token) return t.transport.RoundTrip(req) } diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 3c411f02fd5..33cc3b8ed3a 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -45,7 +45,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features return NewZanzanaClient( fmt.Sprintf("stacks-%s", cfg.StackID), ZanzanaClientConfig{ - Address: cfg.ZanzanaClient.Addr, + URL: cfg.ZanzanaClient.Addr, Token: cfg.ZanzanaClient.Token, TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL, ServerCertFile: cfg.ZanzanaClient.ServerCertFile, @@ -94,7 +94,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features } type ZanzanaClientConfig struct { - Address string + URL string Token string TokenExchangeURL string ServerCertFile string @@ -128,7 +128,7 @@ func NewZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client ), } - conn, err := grpc.NewClient(cfg.Address, dialOptions...) + conn, err := grpc.NewClient(cfg.URL, dialOptions...) if err != nil { return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err) }