oncall-engine/grafana-plugin/pkg/plugin/users.go

282 lines
6.8 KiB
Go
Raw Permalink Normal View History

package plugin
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
type LookupUser struct {
ID int `json:"id"`
Name string `json:"name"`
Login string `json:"login"`
Email string `json:"email"`
AvatarURL string `json:"avatarUrl"`
}
type OrgUser struct {
ID int `json:"userId"`
Name string `json:"name"`
Login string `json:"login"`
Email string `json:"email"`
AvatarURL string `json:"avatarUrl"`
Role string `json:"role"`
}
type OnCallUser struct {
ID int `json:"id"`
Name string `json:"name"`
Login string `json:"login"`
Email string `json:"email"`
Role string `json:"role"`
AvatarURL string `json:"avatar_url"`
Permissions []OnCallPermission `json:"permissions"`
Teams []int `json:"teams"`
}
func (a *OnCallUser) Equal(b *OnCallUser) bool {
if a.ID != b.ID {
return false
}
if a.Name != b.Name {
return false
}
if a.Login != b.Login {
return false
}
if a.Email != b.Email {
return false
}
if a.Role != b.Role {
return false
}
if a.AvatarURL != b.AvatarURL {
return false
}
if len(a.Permissions) != len(b.Permissions) {
return false
}
sort.Slice(a.Permissions, func(i, j int) bool {
return a.Permissions[i].Action < a.Permissions[j].Action
})
sort.Slice(b.Permissions, func(i, j int) bool {
return b.Permissions[i].Action < b.Permissions[j].Action
})
for i := range a.Permissions {
if a.Permissions[i].Action != b.Permissions[i].Action {
return false
}
}
if len(a.Teams) != len(b.Teams) {
return false
}
sort.Slice(a.Teams, func(i, j int) bool {
return a.Teams[i] < a.Teams[j]
})
sort.Slice(b.Teams, func(i, j int) bool {
return b.Teams[i] < b.Teams[j]
})
for i := range a.Teams {
if a.Teams[i] != b.Teams[i] {
return false
}
}
return true
}
type OnCallUserCache struct {
allUsersLock sync.Mutex
allUsersCache map[string]*OnCallUser
allUsersExpiry time.Time
lockInitLock sync.Mutex
userLocks map[string]*sync.Mutex
userCache map[string]*OnCallUser
userExpiry map[string]time.Time
}
const USER_EXPIRY_SECONDS = 60
func NewOnCallUserCache() *OnCallUserCache {
return &OnCallUserCache{
allUsersCache: make(map[string]*OnCallUser),
userLocks: make(map[string]*sync.Mutex),
userCache: make(map[string]*OnCallUser),
userExpiry: make(map[string]time.Time),
}
}
func (c *OnCallUserCache) GetUserLock(user string) *sync.Mutex {
c.lockInitLock.Lock()
defer c.lockInitLock.Unlock()
lock, exists := c.userLocks[user]
if !exists {
lock = &sync.Mutex{}
c.userLocks[user] = lock
}
return lock
}
func (a *App) GetUser(settings *OnCallPluginSettings, user *backend.User) (*OnCallUser, error) {
log.DefaultLogger.Info("GetUser", "user", user)
a.allUsersLock.Lock()
defer a.allUsersLock.Unlock()
if time.Now().Before(a.allUsersExpiry) {
ocu, exists := a.allUsersCache[user.Login]
if !exists {
return nil, fmt.Errorf("user %s not found", user.Login)
}
return ocu, nil
}
users, err := a.GetAllUsers(settings)
if err != nil {
return nil, err
}
var oncallUser *OnCallUser
allUsersCache := make(map[string]*OnCallUser)
for i := range users {
u := &users[i]
allUsersCache[u.Login] = u
if u.Login == user.Login {
oncallUser = u
}
}
a.allUsersCache = allUsersCache
a.allUsersExpiry = time.Now().Add(USER_EXPIRY_SECONDS * time.Second)
if oncallUser == nil {
return nil, fmt.Errorf("user %s not found", user.Login)
}
return oncallUser, nil
}
func (a *App) GetUserForHeader(settings *OnCallPluginSettings, user *backend.User) (*OnCallUser, error) {
userLock := a.GetUserLock(user.Login)
userLock.Lock()
defer userLock.Unlock()
ue, expiryExists := a.userExpiry[user.Login]
if expiryExists && time.Now().Before(ue) {
ocu, userExists := a.userCache[user.Login]
if !userExists {
return nil, fmt.Errorf("user %s not found", user.Login)
}
return ocu, nil
}
onCallUser, err := a.GetUser(settings, user)
if err != nil {
return nil, err
}
// manually created service account with Admin role doesn't have permission to get user teams
if settings.ExternalServiceAccountEnabled {
onCallUser.Teams, err = a.GetTeamsForUser(settings, onCallUser)
if err != nil {
return nil, err
}
}
if settings.RBACEnabled {
onCallUser.Permissions, err = a.GetPermissions(settings, onCallUser)
if err != nil {
return nil, err
}
}
a.userCache[user.Login] = onCallUser
a.userExpiry[user.Login] = time.Now().Add(USER_EXPIRY_SECONDS * time.Second)
return onCallUser, nil
}
func (a *App) GetAllUsers(settings *OnCallPluginSettings) ([]OnCallUser, error) {
atomic.AddInt32(&a.AllUsersCallCount, 1)
reqURL, err := url.Parse(settings.GrafanaURL)
if err != nil {
return nil, fmt.Errorf("error parsing URL: %+v", err)
}
reqURL.Path += "api/org/users"
req, err := http.NewRequest("GET", reqURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("error creating new request: %+v", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", settings.GrafanaToken))
res, err := a.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error making request: %+v", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("error reading response: %+v", err)
}
var result []OrgUser
err = json.Unmarshal(body, &result)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON response: %v body=%v", err, string(body))
}
if res.StatusCode == 200 {
var users []OnCallUser
for _, orgUser := range result {
onCallUser := OnCallUser{
ID: orgUser.ID,
Name: orgUser.Name,
Login: orgUser.Login,
Email: orgUser.Email,
AvatarURL: orgUser.AvatarURL,
Role: orgUser.Role,
}
users = append(users, onCallUser)
}
return users, nil
}
return nil, fmt.Errorf("http status %s", res.Status)
}
func (a *App) GetAllUsersWithPermissions(settings *OnCallPluginSettings) ([]OnCallUser, error) {
onCallUsers, err := a.GetAllUsers(settings)
if err != nil {
return nil, err
}
if settings.RBACEnabled {
permissions, err := a.GetAllPermissions(settings)
if err != nil {
return nil, err
}
for i := range onCallUsers {
userId := strconv.Itoa(onCallUsers[i].ID)
actions, exists := permissions[userId]
if exists {
onCallUsers[i].Permissions = []OnCallPermission{}
for action, _ := range actions {
onCallUsers[i].Permissions = append(onCallUsers[i].Permissions, OnCallPermission{Action: action})
}
} else {
log.DefaultLogger.Error("Did not find permissions for user", "user", onCallUsers[i].Login, "id", userId)
}
}
}
return onCallUsers, nil
}