The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/services/user/userimpl/user.go

537 lines
14 KiB

package userimpl
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/tracing"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type Service struct {
store store
orgService org.Service
teamService team.Service
cacheService *localcache.CacheService
cfg *setting.Cfg
tracer tracing.Tracer
}
func ProvideService(
db db.DB,
orgService org.Service,
Chore: Split get user by ID (#52442) * Remove user from preferences, stars, orguser, team member * Fix lint * Add Delete user from org and dashboard acl * Delete user from user auth * Add DeleteUser to quota * Add test files and adjust user auth store * Rename package in wire for user auth * Import Quota Service interface in other services * do the same in tests * fix lint tests * Fix tests * Add some tests * Rename InsertUser and DeleteUser to InsertOrgUser and DeleteOrgUser * Rename DeleteUser to DeleteByUser in quota * changing a method name in few additional places * Fix in other places * Fix lint * Fix tests * Chore: Split Delete User method * Add fakes for userauth * Add mock for access control Delete User permossion, use interface * Use interface for ream guardian * Add simple fake for dashboard acl * Add go routines, clean up, use interfaces * fix lint * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Split get user by ID * Use new method in api * Add tests * Aplly emthod in auth info service * Fix lint and some tests * Fix get user by ID * Fix lint Remove unused fakes * Use split get user id in admin users * Use GetbyID in cli commands * Clean up after merge * Remove commented out code * Clena up imports * add back ) * Fix wire generation for runner after merge with main Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
3 years ago
cfg *setting.Cfg,
teamService team.Service,
cacheService *localcache.CacheService, tracer tracing.Tracer,
quotaService quota.Service, bundleRegistry supportbundles.Service,
) (user.Service, error) {
store := ProvideStore(db, cfg)
s := &Service{
store: &store,
orgService: orgService,
cfg: cfg,
teamService: teamService,
cacheService: cacheService,
tracer: tracer,
}
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return s, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: quota.TargetSrv(user.QuotaTargetSrv),
DefaultLimits: defaultLimits,
Reporter: s.usage,
}); err != nil {
return s, err
}
bundleRegistry.RegisterSupportItemCollector(s.supportBundleCollector())
return s, nil
}
func (s *Service) GetUsageStats(ctx context.Context) map[string]any {
stats := map[string]any{}
basicAuthStrongPasswordPolicyVal := 0
if s.cfg.BasicAuthStrongPasswordPolicy {
basicAuthStrongPasswordPolicyVal = 1
}
stats["stats.password_policy.count"] = basicAuthStrongPasswordPolicyVal
count, err := s.store.CountUserAccountsWithEmptyRole(ctx)
if err != nil {
return nil
}
stats["stats.user.role_none.count"] = count
return stats
}
func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.Create")
defer span.End()
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
}
// if login is still empty both email and login field is missing
if len(cmd.Login) == 0 {
return nil, user.ErrEmptyUsernameAndEmail.Errorf("user cannot be created with empty username and email")
}
cmdOrg := org.GetOrgIDForNewUserCommand{
Email: cmd.Email,
Login: cmd.Login,
OrgID: cmd.OrgID,
OrgName: cmd.OrgName,
SkipOrgSetup: cmd.SkipOrgSetup,
}
orgID, err := s.orgService.GetIDForNewUser(ctx, cmdOrg)
if err != nil {
return nil, err
}
if cmd.Email == "" {
cmd.Email = cmd.Login
}
if err := s.store.LoginConflict(ctx, cmd.Login, cmd.Email); err != nil {
return nil, user.ErrUserAlreadyExists
}
// create user
usr := &user.User{
UID: cmd.UID,
Email: strings.ToLower(cmd.Email),
Name: cmd.Name,
Login: strings.ToLower(cmd.Login),
Company: cmd.Company,
IsAdmin: cmd.IsAdmin,
IsDisabled: cmd.IsDisabled,
OrgID: orgID,
EmailVerified: cmd.EmailVerified,
Created: timeNow(),
Updated: timeNow(),
LastSeenAt: timeNow().AddDate(-10, 0, 0),
IsServiceAccount: cmd.IsServiceAccount,
}
salt, err := util.GetRandomString(10)
if err != nil {
return nil, err
}
usr.Salt = salt
rands, err := util.GetRandomString(10)
if err != nil {
return nil, err
}
usr.Rands = rands
if len(cmd.Password) > 0 {
if err := cmd.Password.Validate(s.cfg); err != nil {
return nil, err
}
usr.Password, err = cmd.Password.Hash(usr.Salt)
if err != nil {
return nil, err
}
}
_, err = s.store.Insert(ctx, usr)
if err != nil {
return nil, err
}
// create org user link
if !cmd.SkipOrgSetup {
orgUser := org.OrgUser{
OrgID: orgID,
UserID: usr.ID,
Role: org.RoleAdmin,
Created: time.Now(),
Updated: time.Now(),
}
if s.cfg.AutoAssignOrg && !usr.IsAdmin {
if len(cmd.DefaultOrgRole) > 0 {
orgUser.Role = org.RoleType(cmd.DefaultOrgRole)
} else {
orgUser.Role = org.RoleType(s.cfg.AutoAssignOrgRole)
}
}
_, err = s.orgService.InsertOrgUser(ctx, &orgUser)
if err != nil {
err := s.store.Delete(ctx, usr.ID)
return usr, err
}
}
return usr, nil
}
func (s *Service) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error {
ctx, span := s.tracer.Start(ctx, "user.Delete", trace.WithAttributes(
attribute.Int64("userID", cmd.UserID),
))
defer span.End()
_, err := s.store.GetByID(ctx, cmd.UserID)
if err != nil {
Chore: Split get user by ID (#52442) * Remove user from preferences, stars, orguser, team member * Fix lint * Add Delete user from org and dashboard acl * Delete user from user auth * Add DeleteUser to quota * Add test files and adjust user auth store * Rename package in wire for user auth * Import Quota Service interface in other services * do the same in tests * fix lint tests * Fix tests * Add some tests * Rename InsertUser and DeleteUser to InsertOrgUser and DeleteOrgUser * Rename DeleteUser to DeleteByUser in quota * changing a method name in few additional places * Fix in other places * Fix lint * Fix tests * Chore: Split Delete User method * Add fakes for userauth * Add mock for access control Delete User permossion, use interface * Use interface for ream guardian * Add simple fake for dashboard acl * Add go routines, clean up, use interfaces * fix lint * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Split get user by ID * Use new method in api * Add tests * Aplly emthod in auth info service * Fix lint and some tests * Fix get user by ID * Fix lint Remove unused fakes * Use split get user id in admin users * Use GetbyID in cli commands * Clean up after merge * Remove commented out code * Clena up imports * add back ) * Fix wire generation for runner after merge with main Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
3 years ago
return err
}
return s.store.Delete(ctx, cmd.UserID)
}
Chore: Split get user by ID (#52442) * Remove user from preferences, stars, orguser, team member * Fix lint * Add Delete user from org and dashboard acl * Delete user from user auth * Add DeleteUser to quota * Add test files and adjust user auth store * Rename package in wire for user auth * Import Quota Service interface in other services * do the same in tests * fix lint tests * Fix tests * Add some tests * Rename InsertUser and DeleteUser to InsertOrgUser and DeleteOrgUser * Rename DeleteUser to DeleteByUser in quota * changing a method name in few additional places * Fix in other places * Fix lint * Fix tests * Chore: Split Delete User method * Add fakes for userauth * Add mock for access control Delete User permossion, use interface * Use interface for ream guardian * Add simple fake for dashboard acl * Add go routines, clean up, use interfaces * fix lint * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Split get user by ID * Use new method in api * Add tests * Aplly emthod in auth info service * Fix lint and some tests * Fix get user by ID * Fix lint Remove unused fakes * Use split get user id in admin users * Use GetbyID in cli commands * Clean up after merge * Remove commented out code * Clena up imports * add back ) * Fix wire generation for runner after merge with main Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
3 years ago
func (s *Service) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.GetByID", trace.WithAttributes(
attribute.Int64("userID", query.ID),
))
defer span.End()
return s.store.GetByID(ctx, query.ID)
Chore: Split get user by ID (#52442) * Remove user from preferences, stars, orguser, team member * Fix lint * Add Delete user from org and dashboard acl * Delete user from user auth * Add DeleteUser to quota * Add test files and adjust user auth store * Rename package in wire for user auth * Import Quota Service interface in other services * do the same in tests * fix lint tests * Fix tests * Add some tests * Rename InsertUser and DeleteUser to InsertOrgUser and DeleteOrgUser * Rename DeleteUser to DeleteByUser in quota * changing a method name in few additional places * Fix in other places * Fix lint * Fix tests * Chore: Split Delete User method * Add fakes for userauth * Add mock for access control Delete User permossion, use interface * Use interface for ream guardian * Add simple fake for dashboard acl * Add go routines, clean up, use interfaces * fix lint * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Update pkg/services/user/userimpl/user_test.go Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> * Split get user by ID * Use new method in api * Add tests * Aplly emthod in auth info service * Fix lint and some tests * Fix get user by ID * Fix lint Remove unused fakes * Use split get user id in admin users * Use GetbyID in cli commands * Clean up after merge * Remove commented out code * Clena up imports * add back ) * Fix wire generation for runner after merge with main Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
3 years ago
}
func (s *Service) GetByUID(ctx context.Context, query *user.GetUserByUIDQuery) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.GetByUID", trace.WithAttributes(
attribute.Int64("orgID", query.OrgID),
attribute.String("userUID", query.UID),
))
defer span.End()
return s.store.GetByUID(ctx, query.OrgID, query.UID)
}
func (s *Service) GetByLogin(ctx context.Context, query *user.GetUserByLoginQuery) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.GetByLogin")
defer span.End()
return s.store.GetByLogin(ctx, query)
}
func (s *Service) GetByEmail(ctx context.Context, query *user.GetUserByEmailQuery) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.GetByEmail")
defer span.End()
return s.store.GetByEmail(ctx, query)
}
func (s *Service) Update(ctx context.Context, cmd *user.UpdateUserCommand) error {
ctx, span := s.tracer.Start(ctx, "user.Update", trace.WithAttributes(
attribute.Int64("userID", cmd.UserID),
))
defer span.End()
usr, err := s.store.GetByID(ctx, cmd.UserID)
if err != nil {
return err
}
if cmd.OldPassword != nil {
old, err := cmd.OldPassword.Hash(usr.Salt)
if err != nil {
return err
}
if old != usr.Password {
return user.ErrPasswordMissmatch.Errorf("old password does not match stored password")
}
}
if cmd.Password != nil {
if err := cmd.Password.Validate(s.cfg); err != nil {
return err
}
hashed, err := cmd.Password.Hash(usr.Salt)
if err != nil {
return err
}
cmd.Password = &hashed
}
if cmd.OrgID != nil {
orgs, err := s.orgService.GetUserOrgList(ctx, &org.GetUserOrgListQuery{UserID: cmd.UserID})
if err != nil {
return err
}
valid := false
for _, org := range orgs {
if org.OrgID == *cmd.OrgID {
valid = true
}
}
if !valid {
return fmt.Errorf("user does not belong to org")
}
}
return s.store.Update(ctx, cmd)
}
func (s *Service) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLastSeenAtCommand) error {
ctx, span := s.tracer.Start(ctx, "user.UpdateLastSeen", trace.WithAttributes(
attribute.Int64("userID", cmd.UserID),
))
defer span.End()
u, err := s.GetSignedInUser(ctx, &user.GetSignedInUserQuery{
UserID: cmd.UserID,
OrgID: cmd.OrgID,
})
if err != nil {
return err
}
if !s.shouldUpdateLastSeen(u.LastSeenAt) {
return user.ErrLastSeenUpToDate
}
return s.store.UpdateLastSeenAt(ctx, cmd)
}
func (s *Service) shouldUpdateLastSeen(t time.Time) bool {
return time.Since(t) > s.cfg.UserLastSeenUpdateInterval
}
func (s *Service) GetSignedInUser(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
ctx, span := s.tracer.Start(ctx, "user.GetSignedInUser", trace.WithAttributes(
attribute.Int64("userID", query.UserID),
attribute.Int64("orgID", query.OrgID),
))
defer span.End()
var signedInUser *user.SignedInUser
// only check cache if we have a user ID and an org ID in query
if s.cacheService != nil {
if query.OrgID > 0 && query.UserID > 0 {
cacheKey := newSignedInUserCacheKey(query.OrgID, query.UserID)
if cached, found := s.cacheService.Get(cacheKey); found {
cachedUser := cached.(user.SignedInUser)
signedInUser = &cachedUser
return signedInUser, nil
}
}
}
result, err := s.getSignedInUser(ctx, query)
if err != nil {
return nil, err
}
if s.cacheService != nil {
cacheKey := newSignedInUserCacheKey(result.OrgID, result.UserID)
s.cacheService.Set(cacheKey, *result, time.Second*5)
}
return result, nil
}
func newSignedInUserCacheKey(orgID, userID int64) string {
return fmt.Sprintf("signed-in-user-%d-%d", userID, orgID)
}
func (s *Service) getSignedInUser(ctx context.Context, query *user.GetSignedInUserQuery) (*user.SignedInUser, error) {
ctx, span := s.tracer.Start(ctx, "user.getSignedInUser", trace.WithAttributes(
attribute.Int64("userID", query.UserID),
attribute.Int64("orgID", query.OrgID),
))
defer span.End()
usr, err := s.store.GetSignedInUser(ctx, query)
if err != nil {
return nil, err
}
usr.Teams, err = s.teamService.GetTeamIDsByUser(ctx, &team.GetTeamIDsByUserQuery{
OrgID: usr.OrgID,
UserID: usr.UserID,
})
if err != nil {
return nil, err
}
return usr, err
}
func (s *Service) Search(ctx context.Context, query *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) {
ctx, span := s.tracer.Start(ctx, "user.Search", trace.WithAttributes(
attribute.Int64("orgID", query.OrgID),
))
defer span.End()
return s.store.Search(ctx, query)
}
func (s *Service) BatchDisableUsers(ctx context.Context, cmd *user.BatchDisableUsersCommand) error {
ctx, span := s.tracer.Start(ctx, "user.BatchDisableUsers", trace.WithAttributes(
attribute.Int64Slice("userIDs", cmd.UserIDs),
))
defer span.End()
return s.store.BatchDisableUsers(ctx, cmd)
}
func (s *Service) GetProfile(ctx context.Context, query *user.GetUserProfileQuery) (*user.UserProfileDTO, error) {
ctx, span := s.tracer.Start(ctx, "user.GetProfile", trace.WithAttributes(
attribute.Int64("userID", query.UserID),
))
defer span.End()
return s.store.GetProfile(ctx, query)
}
// CreateServiceAccount creates a service account in the user table and adds service account to an organisation in the org_user table
func (s *Service) CreateServiceAccount(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) {
ctx, span := s.tracer.Start(ctx, "user.CreateServiceAccount", trace.WithAttributes(
attribute.Int64("orgID", cmd.OrgID),
))
defer span.End()
cmd.Email = cmd.Login
err := s.store.LoginConflict(ctx, cmd.Login, cmd.Email)
if err != nil {
return nil, serviceaccounts.ErrServiceAccountAlreadyExists.Errorf("service account with login %s already exists", cmd.Login)
}
// create user
usr := &user.User{
Email: cmd.Email,
Name: cmd.Name,
Login: cmd.Login,
IsDisabled: cmd.IsDisabled,
OrgID: cmd.OrgID,
Created: time.Now(),
Updated: time.Now(),
LastSeenAt: time.Now().AddDate(-10, 0, 0),
IsServiceAccount: true,
}
salt, err := util.GetRandomString(10)
if err != nil {
return nil, err
}
usr.Salt = salt
rands, err := util.GetRandomString(10)
if err != nil {
return nil, err
}
usr.Rands = rands
_, err = s.store.Insert(ctx, usr)
if err != nil {
return nil, err
}
// create org user link
orgCmd := &org.AddOrgUserCommand{
OrgID: cmd.OrgID,
UserID: usr.ID,
Role: org.RoleType(cmd.DefaultOrgRole),
AllowAddingServiceAccount: true,
}
if err = s.orgService.AddOrgUser(ctx, orgCmd); err != nil {
return nil, err
}
return usr, nil
}
func (s *Service) supportBundleCollector() supportbundles.Collector {
collectorFn := func(ctx context.Context) (*supportbundles.SupportItem, error) {
query := &user.SearchUsersQuery{
SignedInUser: &user.SignedInUser{
Login: "sa-supportbundle",
OrgRole: "Admin",
IsGrafanaAdmin: true,
IsServiceAccount: true,
Permissions: map[int64]map[string][]string{ac.GlobalOrgID: {ac.ActionUsersRead: {ac.ScopeGlobalUsersAll}}},
},
OrgID: 0,
Query: "",
Page: 0,
Limit: 0,
AuthModule: "",
Filters: []user.Filter{},
IsDisabled: new(bool),
}
res, err := s.Search(ctx, query)
if err != nil {
return nil, err
}
userBytes, err := json.MarshalIndent(res.Users, "", " ")
if err != nil {
return nil, err
}
return &supportbundles.SupportItem{
Filename: "users.json",
FileBytes: userBytes,
}, nil
}
return supportbundles.Collector{
UID: "users",
DisplayName: "User information",
Description: "List users belonging to the Grafana instance",
IncludedByDefault: false,
Default: false,
Fn: collectorFn,
}
}
func (s *Service) usage(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
if used, err := s.store.Count(ctx); err != nil {
return u, err
} else {
tag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope)
if err != nil {
return u, err
}
u.Set(tag, used)
}
return u, nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.User)
return limits, nil
}