# What this PR does
New OnCall plugin initialization process
## Checklist
- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
show up in the autogenerated release notes.
---------
Co-authored-by: Michael Derynck <michael.derynck@grafana.com>
Co-authored-by: Matias Bordese <mbordese@gmail.com>
279 lines
6.7 KiB
Go
279 lines
6.7 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"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", err)
|
|
}
|
|
|
|
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 {
|
|
actions, exists := permissions["1"]
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
return onCallUsers, nil
|
|
}
|