Skip to content

Commit

Permalink
works kinda
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgubler committed Aug 31, 2023
1 parent 20f63d9 commit f354592
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 136 deletions.
57 changes: 7 additions & 50 deletions pkg/keycloakClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ func (this *KeycloakGroup) GetPathElements() []string {
return this.pathElements
}

func (this *KeycloakGroup) IsSameOrganization(other *KeycloakGroup) bool {
if other == nil {
return false
}
return this.GetPathElements()[0] == other.GetPathElements()[0] && this.GetPathElements()[1] == other.GetPathElements()[1]
}

func (this *KeycloakUser) GetDisplayName() string {
if this.FirstName == "" && this.LastName == "" {
return this.Email
Expand Down Expand Up @@ -204,56 +211,6 @@ func (this *KeycloakClient) GetOrganizations(token string) ([]*KeycloakGroup, er
return []*KeycloakGroup{}, nil
}

// FIXME doesn't work properly due to https://github.com/keycloak/keycloak/issues/10348
func (this *KeycloakClient) GetMembers(token string, group *KeycloakGroup) ([]*KeycloakUser, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/auth/admin/realms/%s/groups/%s/members?max=100000", this.baseURL.String(), this.realm, group.Id), nil)
fmt.Printf("%s/auth/admin/realms/%s/groups/%s/members?max=100000\n", this.baseURL.String(), this.realm, group.Id)
if err != nil {
return nil, err
}

req.Header["Authorization"] = []string{"Bearer " + token}
req.Header["cache-control"] = []string{"no-cache"}

r, err := this.client.Do(req)
if err != nil {
return nil, err
}
defer r.Body.Close()

body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}

keycloakUsers := make([]*KeycloakUser, 0)
err = json.Unmarshal(body, &keycloakUsers)
if err != nil {
return nil, err
}
return keycloakUsers, nil
}

// FIXME doesn't work properly due to https://github.com/keycloak/keycloak/issues/10348
// Instead we iterate over all users to fetch their group memberships (see GetGroupMemberships function)
func (this *KeycloakClient) GetAdminUsers(token string) ([]*KeycloakUser, error) {
if this.adminGroup == nil {
groups, err := this.GetGroups(token)
if err != nil {
return nil, err
}
this.findSubgroup(groups)
}
if this.adminGroup == nil {
return nil, errors.New("AdminGroupPath invalid")
}
adminUsers, err := this.GetMembers(token, this.adminGroup)
if err != nil {
return nil, err
}
return adminUsers, nil
}

func (this *KeycloakClient) findSubgroup(groups []*KeycloakGroup) {
for _, group := range groups {
if group.Path == this.adminGroupPath {
Expand Down
45 changes: 39 additions & 6 deletions pkg/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,17 @@ out:
return err
}

/*
grafanaPermissionsMap := getGrafanaPermissionsMap(keycloakUserGroups, keycloakAdmins, keycloakOrganizations)
err = reconcilePermissions(ctx, grafanaPermissionsMap, grafanaOrgsMap, grafanaClient)
if err != nil {
return err
}

err = reconcilePermissions(ctx, grafanaOrgsMap, uidToGrafanaOrgs, adminUids, grafanaClient)
if err != nil {
return err
}
*/
return nil
}

// Generate map with all organizations per user. Key is the user ID, value is an array of pointers to Grafana organizations
// FIXME perhaps get rid of this complication and accept the race condition when creating users
func getControlApiUserOrganizationsMap(keycloakUserGroups map[*KeycloakUser][]*KeycloakGroup, grafanaOrgsMap map[string]*grafana.Org) map[string][]*grafana.Org {
userOrganizations := make(map[string][]*grafana.Org)

Expand Down Expand Up @@ -115,3 +115,36 @@ func getControlApiUserOrganizationsMap(keycloakUserGroups map[*KeycloakUser][]*K

return userOrganizations
}

type GrafanaPermissionSpec struct {
Uid string
PermittedRoles []string
}

// Convert group memberships found in Keycloak into permissions on organizations in Grafana
func getGrafanaPermissionsMap(keycloakUserGroups map[*KeycloakUser][]*KeycloakGroup, keycloakAdmins []*KeycloakUser, keycloakOrganizations []*KeycloakGroup) map[string][]GrafanaPermissionSpec {
permissionsMap := make(map[string][]GrafanaPermissionSpec)
for _, keycloakOrganization := range keycloakOrganizations {
permissionsMap[keycloakOrganization.Name] = []GrafanaPermissionSpec{}

userLoop:
for keycloakUser, groups := range keycloakUserGroups {
// If this user is an admin we ignore any specific organization permissions
for _, admin := range keycloakAdmins {
if admin.Username == keycloakUser.Username {
continue userLoop
}
}
for _, group := range groups {
if keycloakOrganization.IsSameOrganization(group) {
permissionsMap[keycloakOrganization.Name] = append(permissionsMap[keycloakOrganization.Name], GrafanaPermissionSpec{Uid: keycloakUser.Username, PermittedRoles: []string{"Editor", "Viewer"}})
}
}
}

for _, admin := range keycloakAdmins {
permissionsMap[keycloakOrganization.Name] = append(permissionsMap[keycloakOrganization.Name], GrafanaPermissionSpec{Uid: admin.Username, PermittedRoles: []string{"Admin", "Editor", "Viewer"}})
}
}
return permissionsMap
}
114 changes: 34 additions & 80 deletions pkg/reconcilePermissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,53 @@ package controller

import (
"context"
"fmt"
"errors"
grafana "github.com/grafana/grafana-api-golang-client"
"k8s.io/klog/v2"
"k8s.io/utils/strings/slices"
)

func reconcilePermissions(ctx context.Context, grafanaOrgsMap map[string]*grafana.Org, uidToGrafanaOrgs map[string][]*grafana.Org, adminUids []string, grafanaClient *GrafanaClient) error {
// We have to iterate over grafanaOrgs here, hence we need to invert our uid <-> grafanaOrg association table
// FIXME: Shouldn't be necessary anymore with GrafanaClient
grafanaOrgToUids := make(map[*grafana.Org][]string)
for uid, orgs := range uidToGrafanaOrgs {
for _, org := range orgs {
uids, ok := grafanaOrgToUids[org]
if !ok {
grafanaOrgToUids[org] = make([]string, 0)
}
grafanaOrgToUids[org] = append(uids, uid)
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]
if !ok {
return errors.New("Internal error: Keycloak organization not present in Grafana. This shouldn't happen.")
}
}

for _, org := range grafanaOrgsMap {
initialOrgUsers, err := grafanaClient.OrgUsers(org.ID)
initialOrgUsers, err := grafanaClient.OrgUsers(grafanaOrg.ID)
if err != nil {
return err
}
initialOrgUsersMap := make(map[string]grafana.OrgUser)
for _, initialOrgUser := range initialOrgUsers {
if initialOrgUser.Login != "admin" { // ignore admin
initialOrgUsersMap[initialOrgUser.Login] = initialOrgUser
}
}

desiredOrgUsers, ok := grafanaOrgToUids[org]
if !ok {
desiredOrgUsers = make([]string, 0)
}

// debug
fmt.Printf("===== syncing org %s ======\n", org.Name)
fmt.Printf("initial org users: ")
for _, initialOrgUser := range initialOrgUsers {
if initialOrgUser.Role == "Admin" {
continue
for _, permission := range permissions {
var desiredOrgUser *grafana.OrgUser
for i, ou := range initialOrgUsers {
if ou.Login == permission.Uid {
desiredOrgUser = &ou
// remove user from initialOrgUsers array
initialOrgUsers[i] = initialOrgUsers[len(initialOrgUsers)-1]
initialOrgUsers = initialOrgUsers[:len(initialOrgUsers)-1]
break
}
}
fmt.Printf("%s, ", initialOrgUser.Login)
}
fmt.Printf("\n")

fmt.Printf("desired org users: ")
for _, desiredOrgUser := range desiredOrgUsers {
fmt.Printf("%s, ", desiredOrgUser)
}
fmt.Printf("\n")

// Check and if necessary add desired users
for _, desiredOrgUser := range desiredOrgUsers {
if initialOrgUser, ok := initialOrgUsersMap[desiredOrgUser]; ok {
// Permission exists
if initialOrgUser.Role != "Viewer" && initialOrgUser.Role != "Editor" && !slices.Contains(adminUids, initialOrgUser.Login) {
// A normal user can only have roles "Viewer" and "Editor". Other roles would give the user too many permissions, e.g. to change the data source which would be a security issue.
err := grafanaClient.UpdateOrgUser(org.ID, initialOrgUser.UserID, "Editor")
if desiredOrgUser == nil {
klog.Infof("User '%s' should have access to org '%s' (%d), adding", permission.Uid, grafanaOrg.Name, grafanaOrg.ID)
err := grafanaClient.AddOrgUser(grafanaOrg.ID, permission.Uid, permission.PermittedRoles[0])
if err != nil {
// This can happen due to race conditions, hence just a warning
klog.Warning(err)
}
} else {
// orgUser already exists, check if permission is acceptable
if !slices.Contains(permission.PermittedRoles, desiredOrgUser.Role) {
klog.Infof("User '%s' has invalid role on org '%s' (%d), fixing", permission.Uid, grafanaOrg.Name, grafanaOrg.ID)
err := grafanaClient.UpdateOrgUser(grafanaOrg.ID, desiredOrgUser.UserID, permission.PermittedRoles[0])
if err != nil {
// This can happen due to race conditions, hence just a warning
klog.Warning(err)
}
}
} else {
// Permission missing
klog.Infof("User '%s' should have access to org '%s' (%d), adding", desiredOrgUser, org.Name, org.ID)
err := grafanaClient.AddOrgUser(org.ID, desiredOrgUser, "Editor")
if err != nil {
// This can happen due to race conditions, hence just a warning
klog.Warning(err)
}
}
delete(initialOrgUsersMap, desiredOrgUser)

select {
case <-ctx.Done():
Expand All @@ -86,29 +57,12 @@ func reconcilePermissions(ctx context.Context, grafanaOrgsMap map[string]*grafan
}
}

// Check and if necessary add desired admins
for _, desiredOrgAdmin := range adminUids {
if _, ok := initialOrgUsersMap[desiredOrgAdmin]; !ok {
klog.Infof("User '%s' should be admin of org '%s' (%d), adding", desiredOrgAdmin, org.Name, org.ID)
err := grafanaClient.AddOrgUser(org.ID, desiredOrgAdmin, "Admin")
if err != nil {
// This can happen due to race conditions, hence just a warning
klog.Warning(err)
}
}
delete(initialOrgUsersMap, desiredOrgAdmin)

select {
case <-ctx.Done():
return interruptedError
default:
for _, undesiredOrgUser := range initialOrgUsers {
if undesiredOrgUser.Login == "admin" || undesiredOrgUser.Login == grafanaClient.GetUsername() {
continue
}
}

// Remove all the users that are left in initialOrgUsersMap
for _, undesiredOrgUser := range initialOrgUsersMap {
klog.Infof("User '%s' (%d) must not have access to org '%s' (%d), removing", undesiredOrgUser, undesiredOrgUser.UserID, org.Name, org.ID)
err := grafanaClient.RemoveOrgUser(org.ID, undesiredOrgUser.UserID)
klog.Infof("User '%s' (%d) must not have access to org '%s' (%d), removing", undesiredOrgUser.Login, undesiredOrgUser.UserID, grafanaOrg.Name, grafanaOrg.ID)
err := grafanaClient.RemoveOrgUser(grafanaOrg.ID, undesiredOrgUser.UserID)
if err != nil {
// This can happen due to race conditions, hence just a warning
klog.Warning(err)
Expand Down

0 comments on commit f354592

Please sign in to comment.