Skip to content

Commit

Permalink
sync users
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgubler committed Aug 30, 2023
1 parent 604230f commit 20f63d9
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 54 deletions.
4 changes: 2 additions & 2 deletions pkg/grafanaClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
grafana "github.com/grafana/grafana-api-golang-client"
"github.com/hashicorp/go-cleanhttp"
"io"
"net/http"
"net/url"
Expand Down Expand Up @@ -33,7 +32,8 @@ func NewGrafanaClient(baseURL string, cfg grafana.Config) (*GrafanaClient, error
u.User = cfg.BasicAuth
}

cli := cleanhttp.DefaultClient()
tr := &http.Transport{} // Creating the transport explicitly allows for connection pooling and reuse
cli := &http.Client{Transport: tr}

cfg.Client = cli
grafanaClient, err := grafana.New(baseURL, cfg)
Expand Down
35 changes: 30 additions & 5 deletions pkg/keycloakClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ type KeycloakUser struct {
}

type KeycloakGroup struct {
Id string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
SubGroups []*KeycloakGroup `json:"subGroups"`
Attributes *map[string][]string `json:"attributes"`
Id string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
SubGroups []*KeycloakGroup `json:"subGroups"`
Attributes *map[string][]string `json:"attributes"`
pathElements []string `json:"-"` // transient
}

func (this *KeycloakGroup) GetDisplayNameAttribute() string {
Expand All @@ -51,6 +52,30 @@ func (this *KeycloakGroup) GetDisplayNameAttribute() string {
return ""
}

func (this *KeycloakGroup) GetPathElements() []string {
if this.pathElements == nil {
path := this.Path
if strings.HasPrefix(path, "/") {
path = path[1:]
}
this.pathElements = strings.Split(path, "/")
}
return this.pathElements
}

func (this *KeycloakUser) GetDisplayName() string {
if this.FirstName == "" && this.LastName == "" {
return this.Email
}
if this.LastName == "" {
return this.FirstName
}
if this.FirstName == "" {
return this.LastName
}
return this.FirstName + " " + this.LastName
}

func NewKeycloakClient(baseURL string, realm string, username string, password string, clientId string, adminGroupPath string) (*KeycloakClient, error) {
u, err := url.Parse(baseURL)
if err != nil {
Expand Down
58 changes: 31 additions & 27 deletions pkg/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package controller
import (
"context"
"errors"
controlapi "github.com/appuio/control-api/apis/v1"
grafana "github.com/grafana/grafana-api-golang-client"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
)

Expand Down Expand Up @@ -63,18 +61,18 @@ out:
}
defer grafanaClient.CloseIdleConnections()

_, err = reconcileAllOrgs(ctx, keycloakOrganizations, grafanaClient, dashboard)
//grafanaOrgsMap, err := reconcileAllOrgs(ctx, keycloakOrganizations, grafanaClient, dashboard)
grafanaOrgsMap, err := reconcileAllOrgs(ctx, keycloakOrganizations, grafanaClient, dashboard)
if err != nil {
return err
}

uidToGrafanaOrgs := getControlApiUserOrganizationsMap(keycloakUserGroups, grafanaOrgsMap)
err = reconcileUsers(ctx, keycloakUsers, uidToGrafanaOrgs, grafanaClient)
if err != nil {
return err
}

/*
users, err := getControlApiUsersMap(ctx, appuioIoClient)
err = reconcileUsers(ctx, users, uidToGrafanaOrgs, grafanaClient)
if err != nil {
return err
}
err = reconcilePermissions(ctx, grafanaOrgsMap, uidToGrafanaOrgs, adminUids, grafanaClient)
if err != nil {
Expand All @@ -85,29 +83,35 @@ out:
}

// Generate map with all organizations per user. Key is the user ID, value is an array of pointers to Grafana organizations
func getControlApiUserOrganizationsMap(ctx context.Context, appuioIoClient *rest.RESTClient, grafanaOrgsMap map[string]*grafana.Org, adminOrg string) (map[string][]*grafana.Org, []string, error) {
appuioControlApiOrganizationMembers := controlapi.OrganizationMembersList{}
adminUids := make([]string, 0)
err := appuioIoClient.Get().Resource("OrganizationMembers").Do(ctx).Into(&appuioControlApiOrganizationMembers)
if err != nil {
return nil, nil, err
}

func getControlApiUserOrganizationsMap(keycloakUserGroups map[*KeycloakUser][]*KeycloakGroup, grafanaOrgsMap map[string]*grafana.Org) map[string][]*grafana.Org {
userOrganizations := make(map[string][]*grafana.Org)
for _, memberlist := range appuioControlApiOrganizationMembers.Items {
for _, userRef := range memberlist.Spec.UserRefs {
_, ok := userOrganizations[userRef.Name]

for user, groups := range keycloakUserGroups {
groupsLoop:
for _, group := range groups {
if len(group.GetPathElements()) < 2 || group.GetPathElements()[0] != "organizations" {
continue
}
grafanaOrg, ok := grafanaOrgsMap[group.GetPathElements()[1]]
if !ok {
userOrganizations[userRef.Name] = make([]*grafana.Org, 0)
// organization unknown. This shouldn't really happen except in race conditions.
continue
}
grafanaOrg, ok := grafanaOrgsMap[memberlist.Namespace]
if ok {
userOrganizations[userRef.Name] = append(userOrganizations[userRef.Name], grafanaOrg)
// initialize empty list if necessary
_, ok = userOrganizations[user.Username]
if !ok {
userOrganizations[user.Username] = make([]*grafana.Org, 0)
}
if memberlist.Namespace == adminOrg {
adminUids = append(adminUids, userRef.Name)
// check if this grafanaOrg is already in the list. This can happen if a user belongs to multiple subgroups, e.g. "/organizations/inity/accounting" and "/organizations/inity/tech"
for _, o := range userOrganizations[user.Username] {
if o == grafanaOrg {
continue groupsLoop
}
}

userOrganizations[user.Username] = append(userOrganizations[user.Username], grafanaOrg)
}
}
return userOrganizations, adminUids, nil

return userOrganizations
}
9 changes: 4 additions & 5 deletions pkg/reconcileUser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package controller

import (
"crypto/rand"
controlapi "github.com/appuio/control-api/apis/v1"
grafana "github.com/grafana/grafana-api-golang-client"
"math/big"
)
Expand All @@ -22,15 +21,15 @@ func generatePassword() (string, error) {
return pw, nil
}

func createUser(client *GrafanaClient, user controlapi.User, orgs []*grafana.Org) (*grafana.User, error) {
func createUser(client *GrafanaClient, keycloakUser *KeycloakUser, orgs []*grafana.Org) (*grafana.User, error) {
password, err := generatePassword()
if err != nil {
return nil, err
}
grafanaUser := grafana.User{
Email: user.Status.Email,
Login: user.Name,
Name: user.Status.DisplayName,
Email: keycloakUser.Email,
Login: keycloakUser.Username,
Name: keycloakUser.GetDisplayName(),
Password: password,
}
if len(orgs) > 0 {
Expand Down
29 changes: 14 additions & 15 deletions pkg/reconcileUsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package controller

import (
"context"
controlapi "github.com/appuio/control-api/apis/v1"
grafana "github.com/grafana/grafana-api-golang-client"
"k8s.io/klog/v2"
)

func reconcileUsers(ctx context.Context, users map[string]controlapi.User, uidToGrafanaOrgs map[string][]*grafana.Org, grafanaClient *GrafanaClient) error {
func reconcileUsers(ctx context.Context, keycloakUsers []*KeycloakUser, uidToGrafanaOrgs map[string][]*grafana.Org, grafanaClient *GrafanaClient) error {
grafanaUsers, err := grafanaClient.Users()
if err != nil {
return err
Expand All @@ -19,8 +18,8 @@ func reconcileUsers(ctx context.Context, users map[string]controlapi.User, uidTo
}
}

for _, user := range users {
orgs, ok := uidToGrafanaOrgs[user.Name]
for _, keycloakUser := range keycloakUsers {
orgs, ok := uidToGrafanaOrgs[keycloakUser.Username]
if !ok {
// Even if the user doesn't have access to any org we still need to create it.
// This is to work around Grafana's auto_assign_org "feature": If the user were to log in via
Expand All @@ -30,30 +29,30 @@ func reconcileUsers(ctx context.Context, users map[string]controlapi.User, uidTo
}

var grafanaUser *grafana.User
if grafanaUserSearch, ok := grafanaUsersMap[user.Name]; ok {
if grafanaUserSearch.Email != user.Status.Email ||
if grafanaUserSearch, ok := grafanaUsersMap[keycloakUser.Username]; ok {
if grafanaUserSearch.Email != keycloakUser.Email ||
grafanaUserSearch.IsAdmin ||
grafanaUserSearch.Login != user.Name ||
grafanaUserSearch.Name != user.Status.DisplayName {
klog.Infof("User '%s' differs, fixing", user.Name)
grafanaUserSearch.Login != keycloakUser.Username ||
grafanaUserSearch.Name != keycloakUser.GetDisplayName() {
klog.Infof("User '%s' differs, fixing", keycloakUser.Username)
grafanaUser = &grafana.User{
ID: grafanaUserSearch.ID,
IsAdmin: false,
Login: user.Name,
Name: user.Status.DisplayName,
Login: keycloakUser.Username,
Name: keycloakUser.GetDisplayName(),
}
grafanaClient.UserUpdate(*grafanaUser)
}
} else {
klog.Infof("User '%s' is missing, adding", user.Name)
grafanaUser, err = createUser(grafanaClient, user, orgs)
klog.Infof("User '%s' is missing, adding", keycloakUser.Username)
grafanaUser, err = createUser(grafanaClient, keycloakUser, orgs)
if err != nil {
// for now just continue in case errors happen
klog.Error(err)
continue
}
}
delete(grafanaUsersMap, user.Name)
delete(grafanaUsersMap, keycloakUser.Username)

select {
case <-ctx.Done():
Expand All @@ -63,7 +62,7 @@ func reconcileUsers(ctx context.Context, users map[string]controlapi.User, uidTo
}

for _, grafanaUser := range grafanaUsersMap {
klog.Infof("User '%s' (%d) is not in APPUiO Control API or does not have access to any organizations, removing", grafanaUser.Login, grafanaUser.ID)
klog.Infof("User '%s' (%d) not found in Keycloak, removing", grafanaUser.Login, grafanaUser.ID)
grafanaClient.DeleteUser(grafanaUser.ID)

select {
Expand Down

0 comments on commit 20f63d9

Please sign in to comment.