Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use sso settings api instead of app user-values #203

Merged
merged 4 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Add Alertmanager controller

### Changed

- Change SSO settings configuration to use the Grafana admin API instead of app user-values.

## [0.10.1] - 2024-12-12

### Fixed
Expand Down
1 change: 1 addition & 0 deletions api/v1alpha1/grafanaorganization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type GrafanaOrganizationSpec struct {
// DisplayName is the name displayed when viewing the organization in Grafana. It can be different from the actual org's name.
// +kubebuilder:example="Giant Swarm"
// +kubebuilder:validation:MinLength=1
// +kubebuilder:unique=true
DisplayName string `json:"displayName"`

// Access rules defines user permissions for interacting with the organization in Grafana.
Expand Down
84 changes: 34 additions & 50 deletions internal/controller/grafanaorganization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ limitations under the License.
package controller

import (
"cmp"
"context"
"fmt"
"slices"

grafanaAPI "github.com/grafana/grafana-openapi-client-go/client"
"github.com/pkg/errors"
Expand All @@ -38,13 +36,13 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/giantswarm/observability-operator/pkg/config"
grafanaclient "github.com/giantswarm/observability-operator/pkg/grafana/client"
appv1 "github.com/giantswarm/apiextensions-application/api/v1alpha1"

"github.com/giantswarm/observability-operator/api/v1alpha1"
"github.com/giantswarm/observability-operator/internal/controller/predicates"
"github.com/giantswarm/observability-operator/pkg/config"
"github.com/giantswarm/observability-operator/pkg/grafana"
"github.com/giantswarm/observability-operator/pkg/grafana/templating"
grafanaclient "github.com/giantswarm/observability-operator/pkg/grafana/client"
)

// GrafanaOrganizationReconciler reconciles a GrafanaOrganization object
Expand Down Expand Up @@ -138,10 +136,6 @@ func (r *GrafanaOrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error
return []reconcile.Request{}
}

// Sort organizations by orgID to ensure the order is deterministic.
// This is important to prevent incorrect ordering of organizations on grafana restarts.
slices.SortStableFunc(organizations.Items, compareOrganizationsByID)

// Reconcile all grafana organizations when the grafana pod is recreated
requests := make([]reconcile.Request, 0, len(organizations.Items))
for _, organization := range organizations.Items {
Expand All @@ -158,19 +152,6 @@ func (r *GrafanaOrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error
Complete(r)
}

func compareOrganizationsByID(i, j v1alpha1.GrafanaOrganization) int {
// if both orgs have a nil orgID, they are equal
// if one org has a nil orgID, it is higher than the other as it was not created in Grafana yet
if i.Status.OrgID == 0 && j.Status.OrgID == 0 {
return 0
} else if i.Status.OrgID == 0 {
return 1
} else if j.Status.OrgID == 0 {
return -1
}
return cmp.Compare(i.Status.OrgID, j.Status.OrgID)
}

// reconcileCreate creates the grafanaOrganization.
// reconcileCreate ensures the Grafana organization described in grafanaOrganization CR is created in Grafana.
// This function is also responsible for:
Expand Down Expand Up @@ -214,7 +195,7 @@ func (r GrafanaOrganizationReconciler) reconcileCreate(ctx context.Context, graf
}

// Configure Grafana RBAC
if err := r.configureGrafana(ctx); err != nil {
if err := r.configureGrafanaSSO(ctx); err != nil {
return ctrl.Result{}, errors.WithStack(err)
}

Expand Down Expand Up @@ -251,6 +232,9 @@ func newOrganization(grafanaOrganization *v1alpha1.GrafanaOrganization) grafana.
ID: grafanaOrganization.Status.OrgID,
Name: grafanaOrganization.Spec.DisplayName,
TenantIDs: tenantIDs,
Admins: grafanaOrganization.Spec.RBAC.Admins,
Editors: grafanaOrganization.Spec.RBAC.Editors,
Viewers: grafanaOrganization.Spec.RBAC.Viewers,
}
}

Expand Down Expand Up @@ -343,7 +327,7 @@ func (r GrafanaOrganizationReconciler) reconcileDelete(ctx context.Context, graf
}
}

err := r.configureGrafana(ctx)
err := r.configureGrafanaSSO(ctx)
if err != nil {
return errors.WithStack(err)
}
Expand All @@ -367,7 +351,7 @@ func (r GrafanaOrganizationReconciler) reconcileDelete(ctx context.Context, graf
}

// configureGrafana ensures the RBAC configuration is set in Grafana.
func (r *GrafanaOrganizationReconciler) configureGrafana(ctx context.Context) error {
func (r *GrafanaOrganizationReconciler) configureGrafanaSSO(ctx context.Context) error {
logger := log.FromContext(ctx)

organizationList := v1alpha1.GrafanaOrganizationList{}
Expand All @@ -384,34 +368,34 @@ func (r *GrafanaOrganizationReconciler) configureGrafana(ctx context.Context) er
},
}

_, err = controllerutil.CreateOrPatch(ctx, r.Client, grafanaConfig, func() error {
// We always sort the organizations to ensure the order is deterministic and the configmap is stable
// in order to prevent grafana to restarts.
slices.SortStableFunc(organizationList.Items, compareOrganizationsByID)

config, err := templating.GenerateGrafanaConfiguration(organizationList.Items)
if err != nil {
logger.Error(err, "failed to generate grafana user configmap values.")
return errors.WithStack(err)
}

// TODO: to be removed for next release
// cleanup owner references from the config map, see https://github.com/giantswarm/observability-operator/pull/183
for _, organization := range organizationList.Items {
// nolint:errcheck,gosec // ignore errors, owner references are probably already gone
controllerutil.RemoveOwnerReference(&organization, grafanaConfig, r.Scheme)
}

logger.Info("updating grafana-user-values", "config", config)

grafanaConfig.Data = make(map[string]string)
grafanaConfig.Data["values"] = config
// TODO remove after next release start
QuentinBisson marked this conversation as resolved.
Show resolved Hide resolved
if err = r.Client.Delete(ctx, grafanaConfig); client.IgnoreNotFound(err) != nil {
return errors.WithStack(err)
}

return nil
})
// Retrieve the app.
var currentApp appv1.App = appv1.App{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana",
Namespace: "giantswarm",
},
}
err = r.Client.Get(ctx, types.NamespacedName{Name: currentApp.GetName(), Namespace: currentApp.GetNamespace()}, &currentApp)
if err != nil {
return err
}
currentApp.Spec.UserConfig = appv1.AppSpecUserConfig{}
if err = r.Client.Update(ctx, &currentApp); err != nil {
return err
}
QuentinBisson marked this conversation as resolved.
Show resolved Hide resolved

// Configure SSO settings in Grafana
organizations := make([]grafana.Organization, len(organizationList.Items))
for i, organization := range organizationList.Items {
organizations[i] = newOrganization(&organization)
}
err = grafana.ConfigureSSOSettings(ctx, r.GrafanaAPI, organizations)
if err != nil {
logger.Error(err, "failed to configure grafana.")
return errors.WithStack(err)
}

Expand Down
83 changes: 83 additions & 0 deletions pkg/grafana/sso_settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package grafana

import (
"context"
"fmt"
"strings"

"github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-openapi-client-go/models"
"github.com/pkg/errors"
"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
grafanaAdminRole = "Admin"
grafanaEditorRole = "Editor"
grafanaViewerRole = "Viewer"
)

func ConfigureSSOSettings(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organizations []Organization) error {
logger := log.FromContext(ctx)

provider := "generic_oauth"
resp, err := grafanaAPI.SsoSettings.GetProviderSettings(provider, nil)
if err != nil {
logger.Error(err, "failed to get sso provider settings.")
return errors.WithStack(err)
}

orgsMapping := generateGrafanaOrgsMapping(organizations)
settings := resp.Payload.Settings.(map[string]interface{})
settings["role_attribute_path"] = "to_string('Viewer')"
settings["org_attribute_path"] = "groups"
settings["org_mapping"] = orgsMapping

logger.Info("Configuring Grafana SSO settings", "provider", provider, "settings", settings)

// Update the provider settings
_, err = grafanaAPI.SsoSettings.UpdateProviderSettings(provider,
&models.UpdateProviderSettingsParamsBody{
ID: resp.Payload.ID,
Provider: resp.Payload.Provider,
Settings: settings,
})

if err != nil {
logger.Error(err, "failed to configure grafana sso.")
return errors.WithStack(err)
}

return nil
}

func generateGrafanaOrgsMapping(organizations []Organization) string {
var orgMappings []string
// TODO: We need to be admins to be able to see the private dashboards for now, remove the 2 GS groups once https://github.com/giantswarm/roadmap/issues/3696 is done.
// Grant Admin role to Giantswarm users logging in via azure active directory.
orgMappings = append(orgMappings, buildOrgMapping(SharedOrg.Name, "giantswarm-ad:giantswarm-admins", grafanaAdminRole))
// Grant Admin role to Giantswarm users logging in via github.
orgMappings = append(orgMappings, buildOrgMapping(SharedOrg.Name, "giantswarm-github:giantswarm:giantswarm-admins", grafanaAdminRole))
// Grant Editor role to every other users.
orgMappings = append(orgMappings, fmt.Sprintf(`"*:%s:%s"`, SharedOrg.Name, grafanaEditorRole))
for _, organization := range organizations {
for _, adminOrgAttribute := range organization.Admins {
orgMappings = append(orgMappings, buildOrgMapping(organization.Name, adminOrgAttribute, grafanaAdminRole))
}
for _, editorOrgAttribute := range organization.Editors {
orgMappings = append(orgMappings, buildOrgMapping(organization.Name, editorOrgAttribute, grafanaEditorRole))
}
for _, viewerOrgAttribute := range organization.Viewers {
orgMappings = append(orgMappings, buildOrgMapping(organization.Name, viewerOrgAttribute, grafanaViewerRole))
}
}

return strings.Join(orgMappings, " ")
}

func buildOrgMapping(organizationName, userOrgAttribute, role string) string {
// We need to escape the colon in the userOrgAttribute
u := strings.ReplaceAll(userOrgAttribute, ":", "\\:")
// We add double quotes to the org mapping to support spaces in display names
return fmt.Sprintf(`"%s:%s:%s"`, u, organizationName, role)
}

This file was deleted.

77 changes: 0 additions & 77 deletions pkg/grafana/templating/templating.go

This file was deleted.

3 changes: 3 additions & 0 deletions pkg/grafana/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type Organization struct {
ID int64
Name string
TenantIDs []string
Admins []string
Editors []string
Viewers []string
}

type Datasource struct {
Expand Down
Loading