diff --git a/README.md b/README.md index b5b3fde..d3f7803 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,11 @@ The Grafana Organizations Operator only cares about organizations present in Key ### Issues with Grafana -* When a new user is created in Grafana (either by logging in via Keycloak or explicitly created via API), Grafana's `auto_assign_org` "feature" automatically gives the user permission to some organization (whichever is configured). This is almost never what we want. To work around this: +* When a user is created in Grafana, Grafana's `auto_assign_org` "feature" automatically gives the user permission to some organization (whichever is configured). This is almost never what we want. To work around this: * It would be possible to disable `auto_assign_org`, but then Grafana would create a new organization for every new user, which would be even worse. - * We can't let Grafana create users on demand when they first log in (as that would give the user permission to see the `auto_assign_org`), thus we proactively create Grafana users for all known user, thereby avoiding any situations where Grafana would create a user on demand. - * When we create a Grafana user via API we immediately list its group memberships and remove all of them. This is a bit dangerous because it has the potential for race conditions or could leave unwanted permissions behind in case operator fails between the Grafana API calls, but it's the easiest way to handle this. + * We can't create the user and assign the correct permissions before the user logs in for the first time, because upon oauth login Grafana resets all permissions. + * Therefore we just fix permissions after the user has been created by Grafana. This leaves a time gap during which the user can have permissions he shouldn't have, but there isn't much we can do against that. + * A possible improvement would be to configure Grafana such that `auto_assign_org_id` points to a completely empty org, that way the invalid permissions wouldn't matter, but this isn't something this operator can configure. * Because the `grafana-api-golang-client` implementation is incomplete we are wrapping it in the GrafanaClient type and add some functionality. * The Grafana API often ignores the OrgID JSON field. The only workaround for this is to set the HTTP header `x-grafana-org-id`. The GrafanaClient wrapper takes care of this. diff --git a/gen-dev-env.sh b/gen-dev-env.sh index 95e001b..04f2f99 100755 --- a/gen-dev-env.sh +++ b/gen-dev-env.sh @@ -2,7 +2,7 @@ set -e -o pipefail SECRET="$(kubectl --as cluster-admin -n vshn-appuio-grafana get secret grafana-organizations-operator -ojson)" || (>&2 echo "You must be logged in to 'APPUiO Cloud LPG 2' with cluster-admin permissions for this to work" && exit 1) echo -n "" > env -for VAL in KEYCLOAK_ADMIN_GROUP_PATH KEYCLOAK_CLIENT_ID KEYCLOAK_PASSWORD KEYCLOAK_REALM KEYCLOAK_URL KEYCLOAK_USERNAME; do +for VAL in KEYCLOAK_ADMIN_GROUP_PATH KEYCLOAK_AUTO_ASSIGN_ORG_GROUP_PATH KEYCLOAK_CLIENT_ID KEYCLOAK_PASSWORD KEYCLOAK_REALM KEYCLOAK_URL KEYCLOAK_USERNAME; do echo -n "export ${VAL}=\"" >> env echo "${SECRET}" | jq -r ".data.${VAL}" | base64 -d >> env echo "\"" >> env diff --git a/main.go b/main.go index 89cecba..1438894 100644 --- a/main.go +++ b/main.go @@ -15,15 +15,16 @@ import ( ) var ( - GrafanaUrl string - GrafanaUsername string - GrafanaPassword string - KeycloakUrl string - KeycloakRealm string - KeycloakUsername string - KeycloakPassword string - KeycloakClientId string - KeycloakAdminGroupPath string + GrafanaUrl string + GrafanaUsername string + GrafanaPassword string + KeycloakUrl string + KeycloakRealm string + KeycloakUsername string + KeycloakPassword string + KeycloakClientId string + KeycloakAdminGroupPath string + KeycloakAutoAssignOrgGroupPath string ) func main() { @@ -51,18 +52,20 @@ func main() { KeycloakPasswordHidden = "***hidden***" } KeycloakAdminGroupPath = os.Getenv("KEYCLOAK_ADMIN_GROUP_PATH") + KeycloakAutoAssignOrgGroupPath = os.Getenv("KEYCLOAK_AUTO_ASSIGN_ORG_GROUP_PATH") - klog.Infof("GRAFANA_URL: %s\n", GrafanaUrl) - klog.Infof("GRAFANA_USERNAME: %s\n", GrafanaUsername) - klog.Infof("GRAFANA_PASSWORD: %s\n", GrafanaPasswordHidden) - klog.Infof("KEYCLOAK_URL: %s\n", KeycloakUrl) - klog.Infof("KEYCLOAK_REALM: %s\n", KeycloakRealm) - klog.Infof("KEYCLOAK_USERNAME: %s\n", KeycloakUsername) - klog.Infof("KEYCLOAK_PASSWORD: %s\n", KeycloakPasswordHidden) - klog.Infof("KEYCLOAK_CLIENT_ID: %s\n", KeycloakClientId) - klog.Infof("KEYCLOAK_ADMIN_GROUP_PATH: %s\n", KeycloakAdminGroupPath) + klog.Infof("GRAFANA_URL: %s\n", GrafanaUrl) + klog.Infof("GRAFANA_USERNAME: %s\n", GrafanaUsername) + klog.Infof("GRAFANA_PASSWORD: %s\n", GrafanaPasswordHidden) + klog.Infof("KEYCLOAK_URL: %s\n", KeycloakUrl) + klog.Infof("KEYCLOAK_REALM: %s\n", KeycloakRealm) + klog.Infof("KEYCLOAK_USERNAME: %s\n", KeycloakUsername) + klog.Infof("KEYCLOAK_PASSWORD: %s\n", KeycloakPasswordHidden) + klog.Infof("KEYCLOAK_CLIENT_ID: %s\n", KeycloakClientId) + klog.Infof("KEYCLOAK_ADMIN_GROUP_PATH: %s\n", KeycloakAdminGroupPath) + klog.Infof("KEYCLOAK_AUTO_ASSIGN_ORG_GROUP_PATH: %s\n", KeycloakAutoAssignOrgGroupPath) - keycloakClient, err := controller.NewKeycloakClient(KeycloakUrl, KeycloakRealm, KeycloakUsername, KeycloakPassword, KeycloakClientId, KeycloakAdminGroupPath) + keycloakClient, err := controller.NewKeycloakClient(KeycloakUrl, KeycloakRealm, KeycloakUsername, KeycloakPassword, KeycloakClientId, KeycloakAdminGroupPath, KeycloakAutoAssignOrgGroupPath) if err != nil { klog.Errorf("Could not create keycloakClient client: %v\n", err) os.Exit(1) @@ -108,7 +111,7 @@ func main() { for { err = controller.Reconcile(ctx, keycloakClient, grafanaClient, dashboard) select { - case <-time.After(10 * time.Second): + case <-time.After(2 * time.Second): case <-ctx.Done(): os.Exit(0) } diff --git a/pkg/grafanaClient.go b/pkg/grafanaClient.go index 5ebcec3..b7c52a0 100644 --- a/pkg/grafanaClient.go +++ b/pkg/grafanaClient.go @@ -2,11 +2,13 @@ package controller import ( "encoding/json" + "errors" "fmt" grafana "github.com/grafana/grafana-api-golang-client" "io" "net/http" "net/url" + "strconv" ) type UserOrg struct { @@ -49,12 +51,12 @@ func NewGrafanaClient(baseURL string, cfg grafana.Config) (*GrafanaClient, error }, nil } -func (this GrafanaClient) GetUsername() string { +func (this *GrafanaClient) GetUsername() string { return this.config.BasicAuth.Username() } // This method is missing in the grafana-api-golang-client, that's the reason why we're wrapping that client at all -func (this GrafanaClient) GetUserOrgs(user grafana.User) ([]UserOrg, error) { +func (this *GrafanaClient) GetUserOrgs(user grafana.User) ([]UserOrg, error) { url := this.baseURL url.Path = fmt.Sprintf("/api/users/%d/orgs", user.ID) req, err := http.NewRequest("GET", url.String(), nil) @@ -82,93 +84,130 @@ func (this GrafanaClient) GetUserOrgs(user grafana.User) ([]UserOrg, error) { return userOrgs, nil } -func (this GrafanaClient) CloseIdleConnections() { +func (this *GrafanaClient) GetAutoAssignOrgId() (int64, error) { + url := this.baseURL + url.Path = "/api/admin/settings" + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return 0, err + } + password, _ := this.config.BasicAuth.Password() + req.SetBasicAuth(this.config.BasicAuth.Username(), password) + r, err := this.client.Do(req) + defer r.Body.Close() + if err != nil { + return 0, err + } + + body, err := io.ReadAll(r.Body) + if err != nil { + return 0, err + } + + settings := make(map[string]map[string]string) + err = json.Unmarshal(body, &settings) + if err != nil { + return 0, err + } + + settingsUsers, ok := settings["users"] + if ok { + settingsAutoAssignOrgID, ok := settingsUsers["auto_assign_org_id"] + if ok { + return strconv.ParseInt(settingsAutoAssignOrgID, 10, 64) + } + } + + return 0, errors.New("setting users.auto_assign_org_id not found") +} + +func (this *GrafanaClient) CloseIdleConnections() { this.client.CloseIdleConnections() } -func (this GrafanaClient) OrgUsers(orgID int64) ([]grafana.OrgUser, error) { +func (this *GrafanaClient) OrgUsers(orgID int64) ([]grafana.OrgUser, error) { return this.grafanaClient.OrgUsers(orgID) } -func (this GrafanaClient) UpdateOrgUser(orgID, userID int64, role string) error { +func (this *GrafanaClient) UpdateOrgUser(orgID, userID int64, role string) error { return this.grafanaClient.UpdateOrgUser(orgID, userID, role) } -func (this GrafanaClient) AddOrgUser(orgID int64, user, role string) error { +func (this *GrafanaClient) AddOrgUser(orgID int64, user, role string) error { return this.grafanaClient.AddOrgUser(orgID, user, role) } -func (this GrafanaClient) RemoveOrgUser(orgID, userID int64) error { +func (this *GrafanaClient) RemoveOrgUser(orgID, userID int64) error { return this.grafanaClient.RemoveOrgUser(orgID, userID) } -func (this GrafanaClient) CreateUser(user grafana.User) (int64, error) { +func (this *GrafanaClient) CreateUser(user grafana.User) (int64, error) { return this.grafanaClient.CreateUser(user) } -func (this GrafanaClient) Users() (users []grafana.UserSearch, err error) { +func (this *GrafanaClient) Users() (users []grafana.UserSearch, err error) { return this.grafanaClient.Users() } -func (this GrafanaClient) UserUpdate(u grafana.User) error { +func (this *GrafanaClient) UserUpdate(u grafana.User) error { return this.grafanaClient.UserUpdate(u) } -func (this GrafanaClient) DeleteUser(id int64) error { +func (this *GrafanaClient) DeleteUser(id int64) error { return this.grafanaClient.DeleteUser(id) } -func (this GrafanaClient) Orgs() ([]grafana.Org, error) { +func (this *GrafanaClient) Orgs() ([]grafana.Org, error) { return this.grafanaClient.Orgs() } -func (this GrafanaClient) UpdateOrg(id int64, name string) error { +func (this *GrafanaClient) UpdateOrg(id int64, name string) error { return this.grafanaClient.UpdateOrg(id, name) } -func (this GrafanaClient) NewOrg(name string) (int64, error) { +func (this *GrafanaClient) NewOrg(name string) (int64, error) { return this.grafanaClient.NewOrg(name) } -func (this GrafanaClient) Org(id int64) (grafana.Org, error) { +func (this *GrafanaClient) Org(id int64) (grafana.Org, error) { return this.grafanaClient.Org(id) } -func (this GrafanaClient) DeleteOrg(id int64) error { +func (this *GrafanaClient) DeleteOrg(id int64) error { return this.grafanaClient.DeleteOrg(id) } // We don't just wrap this method, we also work around the bad orgID handling of the original library and Grafana API -func (this GrafanaClient) DataSources(org *grafana.Org) ([]*grafana.DataSource, error) { +func (this *GrafanaClient) DataSources(org *grafana.Org) ([]*grafana.DataSource, error) { return this.grafanaClient.WithOrgID(org.ID).DataSources() } // Ditto -func (this GrafanaClient) UpdateDataSource(org *grafana.Org, s *grafana.DataSource) error { +func (this *GrafanaClient) UpdateDataSource(org *grafana.Org, s *grafana.DataSource) error { return this.grafanaClient.WithOrgID(org.ID).UpdateDataSource(s) } // Ditto -func (this GrafanaClient) DeleteDataSource(org *grafana.Org, id int64) error { +func (this *GrafanaClient) DeleteDataSource(org *grafana.Org, id int64) error { return this.grafanaClient.WithOrgID(org.ID).DeleteDataSource(id) } // Ditto -func (this GrafanaClient) NewDataSource(org *grafana.Org, s *grafana.DataSource) (int64, error) { +func (this *GrafanaClient) NewDataSource(org *grafana.Org, s *grafana.DataSource) (int64, error) { return this.grafanaClient.WithOrgID(org.ID).NewDataSource(s) } // Ditto -func (this GrafanaClient) DataSource(org *grafana.Org, id int64) (*grafana.DataSource, error) { +func (this *GrafanaClient) DataSource(org *grafana.Org, id int64) (*grafana.DataSource, error) { return this.grafanaClient.WithOrgID(org.ID).DataSource(id) } // Ditto -func (this GrafanaClient) Dashboards(org *grafana.Org) ([]grafana.FolderDashboardSearchResponse, error) { +func (this *GrafanaClient) Dashboards(org *grafana.Org) ([]grafana.FolderDashboardSearchResponse, error) { return this.grafanaClient.WithOrgID(org.ID).Dashboards() } // Ditto -func (this GrafanaClient) NewDashboard(org *grafana.Org, dashboard grafana.Dashboard) (*grafana.DashboardSaveResponse, error) { +func (this *GrafanaClient) NewDashboard(org *grafana.Org, dashboard grafana.Dashboard) (*grafana.DashboardSaveResponse, error) { return this.grafanaClient.WithOrgID(org.ID).NewDashboard(dashboard) } diff --git a/pkg/keycloakClient.go b/pkg/keycloakClient.go index b5dfdce..d8cc1e6 100644 --- a/pkg/keycloakClient.go +++ b/pkg/keycloakClient.go @@ -14,15 +14,16 @@ import ( ) type KeycloakClient struct { - baseURL url.URL - username string - password string - clientId string - realm string - adminGroupPath string - country string - adminGroup *KeycloakGroup - client *http.Client + baseURL url.URL + username string + password string + clientId string + realm string + adminGroupPath string + autoAssignOrgGroupPath string + country string + adminGroup *KeycloakGroup + client *http.Client } type KeycloakUser struct { @@ -90,7 +91,7 @@ func (this *KeycloakUser) GetDisplayName() string { return this.FirstName + " " + this.LastName } -func NewKeycloakClient(baseURL string, realm string, username string, password string, clientId string, adminGroupPath string) (*KeycloakClient, error) { +func NewKeycloakClient(baseURL string, realm string, username string, password string, clientId string, adminGroupPath string, autoAssignOrgGroupPath string) (*KeycloakClient, error) { u, err := url.Parse(baseURL) if err != nil { return nil, err @@ -103,13 +104,14 @@ func NewKeycloakClient(baseURL string, realm string, username string, password s } return &KeycloakClient{ - baseURL: *u, - client: cli, - realm: realm, - username: username, - password: password, - clientId: clientId, - adminGroupPath: adminGroupPath, + baseURL: *u, + client: cli, + realm: realm, + username: username, + password: password, + clientId: clientId, + adminGroupPath: adminGroupPath, + autoAssignOrgGroupPath: autoAssignOrgGroupPath, }, nil } diff --git a/pkg/reconcile.go b/pkg/reconcile.go index 6ab026e..229362f 100644 --- a/pkg/reconcile.go +++ b/pkg/reconcile.go @@ -24,6 +24,13 @@ func Reconcile(ctx context.Context, keycloakClient *KeycloakClient, grafanaClien } klog.Infof("Found %d users", len(keycloakUsers)) + klog.Infof("Syncing users to Grafana...") + keycloakUsers, err = reconcileUsers(ctx, keycloakUsers, grafanaClient) + if err != nil { + return err + } + klog.Infof("Synced %d users", len(keycloakUsers)) + klog.Infof("Fetching group memberships from Keycloak...") keycloakUserGroups, err := keycloakClient.GetGroupMemberships(keycloakToken, keycloakUsers) if err != nil { @@ -42,30 +49,54 @@ func Reconcile(ctx context.Context, keycloakClient *KeycloakClient, grafanaClien klog.Infof("Extracting admin users...") var keycloakAdmins []*KeycloakUser var keycloakUsersWithoutAdmins []*KeycloakUser -out: +outAdmins: for _, user := range keycloakUsers { for _, group := range keycloakUserGroups[user] { if group.Path == keycloakClient.adminGroupPath { keycloakAdmins = append(keycloakAdmins, user) - continue out + continue outAdmins } } keycloakUsersWithoutAdmins = append(keycloakUsersWithoutAdmins, user) } klog.Infof("Found %d admin users", len(keycloakAdmins)) + klog.Infof("Extracting auto_assign_org users...") + var keycloakAutoAssignOrgUsers []*KeycloakUser +outAutoAssignOrgUsers: + for _, user := range keycloakUsers { + for _, group := range keycloakUserGroups[user] { + if group.Path == keycloakClient.autoAssignOrgGroupPath { + keycloakAutoAssignOrgUsers = append(keycloakAutoAssignOrgUsers, user) + continue outAutoAssignOrgUsers + } + } + } + klog.Infof("Found %d auto_assign_org users", len(keycloakAutoAssignOrgUsers)) + grafanaOrgsMap, err := reconcileAllOrgs(ctx, keycloakOrganizations, grafanaClient, dashboard) if err != nil { return err } - err = reconcileUsers(ctx, keycloakUsers, grafanaClient) + klog.Infof("Checking permissions of normal orgs...") + grafanaPermissionsMap := getGrafanaPermissionsMap(keycloakUserGroups, keycloakAdmins, keycloakOrganizations) + err = reconcilePermissions(ctx, grafanaPermissionsMap, grafanaOrgsMap, grafanaClient) if err != nil { return err } - grafanaPermissionsMap := getGrafanaPermissionsMap(keycloakUserGroups, keycloakAdmins, keycloakOrganizations) - err = reconcilePermissions(ctx, grafanaPermissionsMap, grafanaOrgsMap, grafanaClient) + klog.Infof("Fetching auto_assign_org_id...") + autoAssignOrgId, err := grafanaClient.GetAutoAssignOrgId() + if err != nil { + return err + } + klog.Infof("Checking permissions of auto_assign_org %d", autoAssignOrgId) + var permissions []GrafanaPermissionSpec + for _, keycloakAutoAssignOrgUser := range keycloakAutoAssignOrgUsers { + permissions = append(permissions, GrafanaPermissionSpec{Uid: keycloakAutoAssignOrgUser.Username, PermittedRoles: []string{"Viewer", "Editor", "Admin"}}) + } + err = reconcileSingleOrgPermissions(ctx, permissions, autoAssignOrgId, grafanaClient) if err != nil { return err } diff --git a/pkg/reconcileOrg.go b/pkg/reconcileOrg.go index 6a7da3b..586bff0 100644 --- a/pkg/reconcileOrg.go +++ b/pkg/reconcileOrg.go @@ -5,7 +5,6 @@ import ( grafana "github.com/grafana/grafana-api-golang-client" "k8s.io/klog/v2" "reflect" - "strings" ) // Sync the basic org. Uses the generic Grafana client. @@ -153,81 +152,3 @@ func reconcileOrgDashboard(org *grafana.Org, dataSource *grafana.DataSource, gra } return nil } - -// FIXME obsolete? -func reconcileOrgUsers(org *grafana.Org, client *grafana.Client, desiredUsers []grafana.User, desiredAdmins []grafana.User) error { - orgUsers, err := client.OrgUsersCurrent() - if err != nil { - return err - } - orgUsersMap := make(map[string]grafana.OrgUser) - for _, orgUser := range orgUsers { - orgUsersMap[orgUser.Login] = orgUser - } - - // Sync all non-admin users (viewer, editor) - for _, desiredUser := range desiredUsers { - if userListContains(desiredAdmins, desiredUser) { - // we'll add this user later as an admin - continue - } - if orgUser, ok := orgUsersMap[desiredUser.Login]; ok { - if !strings.EqualFold(orgUser.Role, "viewer") && !strings.EqualFold(orgUser.Role, "editor") { - klog.Infof("Organization %d user %s has invalid role %s, fixing", org.ID, desiredUser.Login, orgUser.Role) - err = client.UpdateOrgUser(org.ID, orgUser.UserID, "editor") - if err != nil { - return err - } - } - delete(orgUsersMap, desiredUser.Login) - } else { - klog.Infof("Organization %d user %s is missing, adding", org.ID, desiredUser.Login) - err = client.AddOrgUser(org.ID, desiredUser.Login, "editor") - if err != nil { - return err - } - } - } - - // Sync all admin users - for _, desiredAdmin := range desiredAdmins { - if orgUser, ok := orgUsersMap[desiredAdmin.Login]; ok { - if !strings.EqualFold(orgUser.Role, "admin") { - klog.Infof("Organization %d admin %s has invalid role %s, fixing", org.ID, desiredAdmin.Login, orgUser.Role) - err = client.UpdateOrgUser(org.ID, orgUser.UserID, "admin") - if err != nil { - return err - } - } - delete(orgUsersMap, desiredAdmin.Login) - } else { - klog.Infof("Organization %d admin %s is missing, adding", org.ID, desiredAdmin.Login) - err = client.AddOrgUser(org.ID, desiredAdmin.Login, "admin") - if err != nil { - return err - } - } - } - - delete(orgUsersMap, "admin") // don't delete the admin user... - - for _, removeUser := range orgUsersMap { - klog.Infof("Organization %d user %s should not be there, removing", org.ID, removeUser.Login) - err = client.RemoveOrgUser(org.ID, removeUser.UserID) - if err != nil { - return err - } - } - - return nil -} - -// FIXME obsolete? -func userListContains(userList []grafana.User, user grafana.User) bool { - for _, entry := range userList { - if entry.ID == user.ID && entry.Login == user.Login { - return true - } - } - return false -} diff --git a/pkg/reconcilePermissions.go b/pkg/reconcilePermissions.go index d01b6e3..73f9432 100644 --- a/pkg/reconcilePermissions.go +++ b/pkg/reconcilePermissions.go @@ -8,6 +8,21 @@ import ( "k8s.io/utils/strings/slices" ) +func reconcileSingleOrgPermissions(ctx context.Context, grafanaPermissions []GrafanaPermissionSpec, grafanaOrgId int64, grafanaClient *GrafanaClient) error { + grafanaOrg, err := grafanaClient.Org(grafanaOrgId) + if err != nil { + return err + } + + grafanaPermissionsMap := make(map[string][]GrafanaPermissionSpec) + grafanaPermissionsMap["auto_assign_org"] = grafanaPermissions + + grafanaOrgsMap := make(map[string]*grafana.Org) + grafanaOrgsMap["auto_assign_org"] = &grafanaOrg + + return reconcilePermissions(ctx, grafanaPermissionsMap, grafanaOrgsMap, grafanaClient) +} + func reconcilePermissions(ctx context.Context, grafanaPermissionsMap map[string][]GrafanaPermissionSpec, grafanaOrgsMap map[string]*grafana.Org, grafanaClient *GrafanaClient) error { for orgName, permissions := range grafanaPermissionsMap { grafanaOrg, ok := grafanaOrgsMap[orgName] diff --git a/pkg/reconcileUsers.go b/pkg/reconcileUsers.go index 1ff3657..e6eb5f1 100644 --- a/pkg/reconcileUsers.go +++ b/pkg/reconcileUsers.go @@ -6,10 +6,11 @@ import ( "k8s.io/klog/v2" ) -func reconcileUsers(ctx context.Context, keycloakUsers []*KeycloakUser, grafanaClient *GrafanaClient) error { +func reconcileUsers(ctx context.Context, keycloakUsers []*KeycloakUser, grafanaClient *GrafanaClient) ([]*KeycloakUser, error) { + var syncedUsers []*KeycloakUser grafanaUsers, err := grafanaClient.Users() if err != nil { - return err + return nil, err } grafanaUsersMap := make(map[string]grafana.UserSearch) for _, grafanaUser := range grafanaUsers { @@ -35,20 +36,27 @@ func reconcileUsers(ctx context.Context, keycloakUsers []*KeycloakUser, grafanaC } grafanaClient.UserUpdate(*grafanaUser) } - } else { - klog.Infof("User '%s' is missing, adding", keycloakUser.Username) - grafanaUser, err = createUser(grafanaClient, keycloakUser) - if err != nil { - // for now just continue in case errors happen - klog.Error(err) - continue - } + syncedUsers = append(syncedUsers, keycloakUser) } + // For now we do not create users in Grafana. + // The original thought of this was that it would be possible to set up the users and permissions before the user logs in for the first time, therefore providing him/her with correct permissions upon first login. + // Turns out this doesn't work, as with the first OAuth login Grafana resets all permissions of the user, breaking this entire scheme. + // Instead now we let Grafana create the user with invalid permissions, then we go and fix the permissions. + /* + else { + klog.Infof("User '%s' is missing, adding", keycloakUser.Username) + grafanaUser, err = createUser(grafanaClient, keycloakUser) + if err != nil { + // for now just continue in case errors happen + klog.Error(err) + continue + } + }*/ delete(grafanaUsersMap, keycloakUser.Username) select { case <-ctx.Done(): - return interruptedError + return nil, interruptedError default: } } @@ -59,10 +67,10 @@ func reconcileUsers(ctx context.Context, keycloakUsers []*KeycloakUser, grafanaC select { case <-ctx.Done(): - return interruptedError + return nil, interruptedError default: } } - return nil + return syncedUsers, nil }